boris-behav-obs 8.16.5__py3-none-any.whl → 9.7.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- boris/__init__.py +1 -1
- boris/__main__.py +1 -1
- boris/about.py +28 -40
- boris/add_modifier.py +88 -80
- boris/add_modifier_ui.py +266 -144
- boris/advanced_event_filtering.py +23 -29
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_export_to_feral.py +225 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +235 -236
- boris/behavior_binary_table.py +33 -50
- boris/behaviors_coding_map.py +17 -18
- boris/boris_cli.py +6 -25
- boris/cmd_arguments.py +12 -1
- boris/coding_pad.py +19 -36
- boris/config.py +109 -50
- boris/config_file.py +58 -67
- boris/connections.py +105 -58
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2174 -1303
- boris/core_qrc.py +15892 -10829
- boris/core_ui.py +941 -806
- boris/db_functions.py +17 -42
- boris/dev.py +27 -7
- boris/dialog.py +461 -242
- boris/duration_widget.py +9 -14
- boris/edit_event.py +61 -31
- boris/edit_event_ui.py +208 -97
- boris/event_operations.py +405 -281
- boris/events_cursor.py +25 -17
- boris/events_snapshots.py +36 -82
- boris/exclusion_matrix.py +4 -9
- boris/export_events.py +180 -203
- boris/export_observation.py +60 -73
- boris/external_processes.py +123 -98
- boris/geometric_measurement.py +427 -218
- boris/gui_utilities.py +91 -14
- boris/image_overlay.py +4 -4
- boris/import_observations.py +190 -98
- boris/ipc_mpv.py +325 -0
- boris/irr.py +20 -57
- boris/latency.py +31 -24
- boris/measurement_widget.py +14 -18
- boris/media_file.py +17 -19
- boris/menu_options.py +16 -6
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +7 -9
- boris/mpv2.py +128 -35
- boris/observation.py +501 -211
- boris/observation_operations.py +1037 -393
- boris/observation_ui.py +573 -363
- boris/observations_list.py +51 -58
- boris/otx_parser.py +74 -68
- boris/param_panel.py +45 -59
- boris/param_panel_ui.py +254 -138
- boris/player_dock_widget.py +91 -56
- boris/plot_data_module.py +20 -53
- boris/plot_events.py +56 -153
- boris/plot_events_rt.py +16 -30
- boris/plot_spectrogram_rt.py +83 -56
- boris/plot_waveform_rt.py +27 -49
- boris/plugins.py +468 -0
- boris/portion/__init__.py +18 -8
- boris/portion/const.py +35 -18
- boris/portion/dict.py +5 -5
- boris/portion/func.py +2 -2
- boris/portion/interval.py +21 -41
- boris/portion/io.py +41 -32
- boris/preferences.py +307 -123
- boris/preferences_ui.py +686 -227
- boris/project.py +294 -271
- boris/project_functions.py +626 -537
- boris/project_import_export.py +204 -213
- boris/project_ui.py +673 -441
- boris/qrc_boris.py +6 -3
- boris/qrc_boris5.py +6 -3
- boris/select_modifiers.py +62 -90
- boris/select_observations.py +19 -197
- boris/select_subj_behav.py +67 -39
- boris/state_events.py +51 -33
- boris/subjects_pad.py +7 -9
- boris/synthetic_time_budget.py +42 -26
- boris/time_budget_functions.py +169 -169
- boris/time_budget_widget.py +77 -89
- boris/transitions.py +41 -41
- boris/utilities.py +594 -226
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +86 -28
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +240 -136
- boris_behav_obs-9.7.12.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.12.dist-info/RECORD +110 -0
- {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.12.dist-info/entry_points.txt +2 -0
- boris/README.TXT +0 -22
- boris/add_modifier.ui +0 -323
- boris/converters.ui +0 -289
- boris/core.qrc +0 -37
- boris/core.ui +0 -1571
- boris/edit_event.ui +0 -233
- boris/icons/logo_eye.ico +0 -0
- boris/map_creator.py +0 -982
- boris/observation.ui +0 -814
- boris/param_panel.ui +0 -379
- boris/preferences.ui +0 -537
- boris/project.ui +0 -1074
- boris/vlc_local.py +0 -90
- boris_behav_obs-8.16.5.dist-info/LICENSE.TXT +0 -674
- boris_behav_obs-8.16.5.dist-info/METADATA +0 -134
- boris_behav_obs-8.16.5.dist-info/RECORD +0 -107
- boris_behav_obs-8.16.5.dist-info/entry_points.txt +0 -2
- {boris → boris_behav_obs-9.7.12.dist-info/licenses}/LICENSE.TXT +0 -0
- {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.dist-info}/top_level.txt +0 -0
boris/observation.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This file is part of BORIS.
|
|
7
7
|
|
|
@@ -20,15 +20,14 @@ This file is part of BORIS.
|
|
|
20
20
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
-
import glob
|
|
24
23
|
import logging
|
|
25
24
|
import os
|
|
26
25
|
import pandas as pd
|
|
27
26
|
import pathlib as pl
|
|
28
27
|
|
|
29
|
-
from
|
|
30
|
-
from
|
|
31
|
-
from
|
|
28
|
+
from PySide6.QtCore import Qt
|
|
29
|
+
from PySide6.QtGui import QColor
|
|
30
|
+
from PySide6.QtWidgets import (
|
|
32
31
|
QDialog,
|
|
33
32
|
QVBoxLayout,
|
|
34
33
|
QHBoxLayout,
|
|
@@ -43,10 +42,11 @@ from PyQt5.QtWidgets import (
|
|
|
43
42
|
QApplication,
|
|
44
43
|
QMenu,
|
|
45
44
|
QListWidgetItem,
|
|
45
|
+
QHeaderView,
|
|
46
46
|
)
|
|
47
47
|
|
|
48
48
|
from . import config as cfg
|
|
49
|
-
from . import dialog,
|
|
49
|
+
from . import dialog, plot_data_module, project_functions
|
|
50
50
|
from . import utilities as util
|
|
51
51
|
from . import gui_utilities
|
|
52
52
|
from .observation_ui import Ui_Form
|
|
@@ -76,7 +76,10 @@ class AssignConverter(QDialog):
|
|
|
76
76
|
self.cbb[-1].addItems(["None"] + sorted(converters.keys()))
|
|
77
77
|
|
|
78
78
|
if column_idx in col_conv:
|
|
79
|
-
|
|
79
|
+
if col_conv[column_idx] in (["None"] + sorted(converters.keys())):
|
|
80
|
+
self.cbb[-1].setCurrentIndex((["None"] + sorted(converters.keys())).index(col_conv[column_idx]))
|
|
81
|
+
else:
|
|
82
|
+
self.cbb[-1].setCurrentIndex(0)
|
|
80
83
|
else:
|
|
81
84
|
self.cbb[-1].setCurrentIndex(0)
|
|
82
85
|
hbox.addWidget(self.cbb[-1])
|
|
@@ -97,7 +100,7 @@ class AssignConverter(QDialog):
|
|
|
97
100
|
|
|
98
101
|
|
|
99
102
|
class Observation(QDialog, Ui_Form):
|
|
100
|
-
def __init__(self, tmp_dir, project_path="", converters={}, time_format=cfg.S, parent=None):
|
|
103
|
+
def __init__(self, tmp_dir: str, project_path: str = "", converters: dict = {}, time_format: str = cfg.S, parent=None):
|
|
101
104
|
"""
|
|
102
105
|
Args:
|
|
103
106
|
tmp_dir (str): path of temporary directory
|
|
@@ -111,22 +114,31 @@ class Observation(QDialog, Ui_Form):
|
|
|
111
114
|
self.project_path = project_path
|
|
112
115
|
self.converters = converters
|
|
113
116
|
self.time_format = time_format
|
|
114
|
-
self.observation_time_interval = [0, 0]
|
|
117
|
+
self.observation_time_interval: tuple = [0, 0]
|
|
115
118
|
self.mem_dir = ""
|
|
116
119
|
self.test = None
|
|
117
120
|
|
|
118
121
|
self.setupUi(self)
|
|
119
122
|
|
|
120
123
|
# insert duration widget for time offset
|
|
121
|
-
self.obs_time_offset = duration_widget.Duration_widget(0)
|
|
124
|
+
# self.obs_time_offset = duration_widget.Duration_widget(0)
|
|
125
|
+
self.obs_time_offset = dialog.get_time_widget(0)
|
|
122
126
|
self.horizontalLayout_6.insertWidget(1, self.obs_time_offset)
|
|
127
|
+
self.obs_time_offset.setEnabled(False)
|
|
128
|
+
|
|
129
|
+
# time offset
|
|
130
|
+
self.cb_time_offset.stateChanged.connect(self.cb_time_offset_changed)
|
|
131
|
+
# date offset
|
|
132
|
+
"""self.cb_date_offset.stateChanged.connect(self.cb_date_offset_changed)"""
|
|
123
133
|
|
|
124
134
|
# observation type
|
|
125
135
|
self.rb_media_files.toggled.connect(self.obs_type_changed)
|
|
126
136
|
self.rb_live.toggled.connect(self.obs_type_changed)
|
|
127
137
|
self.rb_images.toggled.connect(self.obs_type_changed)
|
|
128
138
|
|
|
129
|
-
|
|
139
|
+
# button menu for media
|
|
140
|
+
|
|
141
|
+
add_media_menu_items = [
|
|
130
142
|
"media abs path|with absolute path",
|
|
131
143
|
"media rel path|with relative path",
|
|
132
144
|
{
|
|
@@ -136,23 +148,68 @@ class Observation(QDialog, Ui_Form):
|
|
|
136
148
|
]
|
|
137
149
|
},
|
|
138
150
|
]
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
151
|
+
|
|
152
|
+
self.media_menu = QMenu()
|
|
153
|
+
# Add actions to the menu
|
|
154
|
+
"""
|
|
155
|
+
self.action1 = QAction("with absolute path")
|
|
156
|
+
self.action2 = QAction("with relative path")
|
|
157
|
+
self.action3 = QAction("directory with absolute path")
|
|
158
|
+
self.action4 = QAction("directory with relative path")
|
|
159
|
+
|
|
160
|
+
self.menu.addAction(self.action1)
|
|
161
|
+
self.menu.addAction(self.action2)
|
|
162
|
+
self.menu.addAction(self.action3)
|
|
163
|
+
self.menu.addAction(self.action4)
|
|
164
|
+
|
|
165
|
+
# Connect actions to functions
|
|
166
|
+
self.action1.triggered.connect(lambda: self.add_media(mode="media abs path|with absolute path"))
|
|
167
|
+
self.action2.triggered.connect(lambda: self.add_media(mode="media rel path|with relative path"))
|
|
168
|
+
self.action3.triggered.connect(lambda: self.add_media(mode="dir abs path|with absolute path"))
|
|
169
|
+
self.action4.triggered.connect(lambda: self.add_media(mode="dir rel path|wih relative path"))
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
self.media_menu.triggered.connect(lambda x: self.add_media(mode=x.statusTip()))
|
|
173
|
+
self.add_button_menu(add_media_menu_items, self.media_menu)
|
|
174
|
+
self.pbAddVideo.setMenu(self.media_menu)
|
|
143
175
|
|
|
144
176
|
self.pbRemoveVideo.clicked.connect(self.remove_media)
|
|
145
177
|
|
|
146
|
-
#
|
|
178
|
+
# button menu for data file
|
|
147
179
|
data_menu_items = [
|
|
148
180
|
"data abs path|with absolute path",
|
|
149
181
|
"data rel path|with relative path",
|
|
150
182
|
]
|
|
151
183
|
|
|
152
|
-
menu_data = QMenu()
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
184
|
+
self.menu_data = QMenu()
|
|
185
|
+
|
|
186
|
+
# Add actions to the menu
|
|
187
|
+
"""
|
|
188
|
+
self.data_action1 = QAction("with absolute path")
|
|
189
|
+
self.data_action2 = QAction("with relative path")
|
|
190
|
+
self.menu_data.addAction(self.data_action1)
|
|
191
|
+
self.menu_data.addAction(self.data_action2)
|
|
192
|
+
|
|
193
|
+
# Connect actions to functions
|
|
194
|
+
self.data_action1.triggered.connect(lambda: self.add_data_file(mode="data abs path|with absolute path"))
|
|
195
|
+
self.data_action2.triggered.connect(lambda: self.add_data_file(mode="data rel path|with relative path"))
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
self.menu_data.triggered.connect(lambda x: self.add_data_file(mode=x.statusTip()))
|
|
199
|
+
self.add_button_menu(data_menu_items, self.menu_data)
|
|
200
|
+
self.pb_add_data_file.setMenu(self.menu_data)
|
|
201
|
+
|
|
202
|
+
# button menu for images
|
|
203
|
+
images_menu_items = [
|
|
204
|
+
"images abs path|with absolute path",
|
|
205
|
+
"images rel path|with relative path",
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
self.menu_images = QMenu()
|
|
209
|
+
|
|
210
|
+
self.menu_images.triggered.connect(lambda x: self.add_images_directory(mode=x.statusTip()))
|
|
211
|
+
self.add_button_menu(images_menu_items, self.menu_images)
|
|
212
|
+
self.pb_add_directory.setMenu(self.menu_images)
|
|
156
213
|
|
|
157
214
|
self.pb_remove_data_file.clicked.connect(self.remove_data_file)
|
|
158
215
|
self.pb_view_data_head.clicked.connect(self.view_data_file_head_tail)
|
|
@@ -163,6 +220,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
163
220
|
|
|
164
221
|
self.cbVisualizeSpectrogram.clicked.connect(self.extract_wav)
|
|
165
222
|
self.cb_visualize_waveform.clicked.connect(self.extract_wav)
|
|
223
|
+
|
|
166
224
|
self.cb_observation_time_interval.clicked.connect(self.limit_time_interval)
|
|
167
225
|
|
|
168
226
|
self.pbSave.clicked.connect(self.pbSave_clicked)
|
|
@@ -170,21 +228,27 @@ class Observation(QDialog, Ui_Form):
|
|
|
170
228
|
self.pbCancel.clicked.connect(self.pbCancel_clicked)
|
|
171
229
|
|
|
172
230
|
self.tw_data_files.cellDoubleClicked[int, int].connect(self.tw_data_files_cellDoubleClicked)
|
|
231
|
+
self.tw_data_files.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
|
|
173
232
|
|
|
174
|
-
self.
|
|
233
|
+
self.twVideo1.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
|
|
175
234
|
|
|
176
|
-
self.
|
|
177
|
-
|
|
178
|
-
|
|
235
|
+
self.mediaDurations, self.mediaFPS, self.mediaHasVideo, self.mediaHasAudio, self.media_creation_time = {}, {}, {}, {}, {}
|
|
236
|
+
|
|
237
|
+
for w in (
|
|
238
|
+
self.cbVisualizeSpectrogram,
|
|
239
|
+
self.cb_visualize_waveform,
|
|
240
|
+
self.cb_observation_time_interval,
|
|
241
|
+
self.cb_media_creation_date_as_offset,
|
|
242
|
+
self.cbCloseCurrentBehaviorsBetweenVideo,
|
|
243
|
+
):
|
|
244
|
+
w.setEnabled(False)
|
|
179
245
|
|
|
180
|
-
|
|
181
|
-
self.cbCloseCurrentBehaviorsBetweenVideo.setChecked(False)
|
|
182
|
-
self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
|
|
246
|
+
self.cb_observation_time_interval.setEnabled(True)
|
|
183
247
|
|
|
184
248
|
self.cb_start_from_current_time.stateChanged.connect(self.cb_start_from_current_time_changed)
|
|
185
249
|
|
|
186
250
|
# images
|
|
187
|
-
self.pb_add_directory.clicked.connect(self.add_images_directory)
|
|
251
|
+
# self.pb_add_directory.clicked.connect(self.add_images_directory)
|
|
188
252
|
self.pb_remove_directory.clicked.connect(self.remove_images_directory)
|
|
189
253
|
|
|
190
254
|
self.tabWidget.setCurrentIndex(0)
|
|
@@ -192,6 +256,60 @@ class Observation(QDialog, Ui_Form):
|
|
|
192
256
|
# geometry
|
|
193
257
|
gui_utilities.restore_geometry(self, "new observation", (800, 650))
|
|
194
258
|
|
|
259
|
+
# def cb_date_offset_changed(self):
|
|
260
|
+
# """
|
|
261
|
+
# activate/desactivate time value
|
|
262
|
+
# """
|
|
263
|
+
# self.de_date_offset.setEnabled(self.cb_date_offset.isChecked())
|
|
264
|
+
|
|
265
|
+
def check_media_creation_date(self):
|
|
266
|
+
"""
|
|
267
|
+
check if all media files contain creation date time
|
|
268
|
+
search in metadata then in filename
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
creation_date_not_found: list = []
|
|
272
|
+
flag_filename_used = False
|
|
273
|
+
|
|
274
|
+
self.media_creation_time = {}
|
|
275
|
+
|
|
276
|
+
if self.cb_media_creation_date_as_offset.isChecked():
|
|
277
|
+
for row in range(self.twVideo1.rowCount()):
|
|
278
|
+
if self.twVideo1.item(row, 2).text(): # media file path
|
|
279
|
+
date_time_original = util.extract_video_creation_date(
|
|
280
|
+
project_functions.full_path(self.twVideo1.item(row, 2).text(), self.project_path)
|
|
281
|
+
)
|
|
282
|
+
if date_time_original is None:
|
|
283
|
+
date_time_file_name = util.extract_date_time_from_file_name(self.twVideo1.item(row, 2).text())
|
|
284
|
+
if date_time_file_name is None:
|
|
285
|
+
creation_date_not_found.append(self.twVideo1.item(row, 2).text())
|
|
286
|
+
else:
|
|
287
|
+
self.media_creation_time[self.twVideo1.item(row, 2).text()] = date_time_file_name
|
|
288
|
+
flag_filename_used = True
|
|
289
|
+
else:
|
|
290
|
+
self.media_creation_time[self.twVideo1.item(row, 2).text()] = date_time_original
|
|
291
|
+
|
|
292
|
+
if creation_date_not_found:
|
|
293
|
+
QMessageBox.warning(
|
|
294
|
+
self, cfg.programName, "The creation date time was not found for all media file(s).\nThe option was disabled."
|
|
295
|
+
)
|
|
296
|
+
self.cb_media_creation_date_as_offset.setChecked(False)
|
|
297
|
+
self.media_creation_time = {}
|
|
298
|
+
return 1
|
|
299
|
+
|
|
300
|
+
elif flag_filename_used:
|
|
301
|
+
QMessageBox.information(
|
|
302
|
+
self, cfg.programName, "The creation date time was not found in metadata. The media file name(s) was/were used"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return 0
|
|
306
|
+
|
|
307
|
+
def cb_time_offset_changed(self):
|
|
308
|
+
"""
|
|
309
|
+
activate/desactivate date value
|
|
310
|
+
"""
|
|
311
|
+
self.obs_time_offset.setEnabled(self.cb_time_offset.isChecked())
|
|
312
|
+
|
|
195
313
|
def use_media_file_name_as_obsid(self) -> None:
|
|
196
314
|
"""
|
|
197
315
|
set observation id with the media file name value (without path)
|
|
@@ -200,7 +318,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
200
318
|
QMessageBox.critical(self, cfg.programName, "A media file must be loaded in player #1")
|
|
201
319
|
return
|
|
202
320
|
|
|
203
|
-
first_media_file = ""
|
|
321
|
+
first_media_file: str = ""
|
|
204
322
|
for row in range(self.twVideo1.rowCount()):
|
|
205
323
|
if int(self.twVideo1.cellWidget(row, 0).currentText()) == 1:
|
|
206
324
|
first_media_file = self.twVideo1.item(row, 2).text()
|
|
@@ -235,11 +353,39 @@ class Observation(QDialog, Ui_Form):
|
|
|
235
353
|
# hide 'limit observation to time interval' for images
|
|
236
354
|
self.cb_observation_time_interval.setEnabled(not self.rb_images.isChecked())
|
|
237
355
|
|
|
238
|
-
def add_images_directory(self):
|
|
356
|
+
def add_images_directory(self, mode: str):
|
|
239
357
|
"""
|
|
240
358
|
add path to images directory
|
|
241
359
|
"""
|
|
242
|
-
|
|
360
|
+
|
|
361
|
+
if mode.split("|")[0] not in (
|
|
362
|
+
"images abs path",
|
|
363
|
+
"images rel path",
|
|
364
|
+
):
|
|
365
|
+
QMessageBox.critical(
|
|
366
|
+
self,
|
|
367
|
+
cfg.programName,
|
|
368
|
+
(f"Wrong mode to add a pictures directory {mode}"),
|
|
369
|
+
)
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
# check if project saved
|
|
373
|
+
if (" w/o" in mode or " rel " in mode) and (not self.project_file_name):
|
|
374
|
+
QMessageBox.critical(
|
|
375
|
+
self,
|
|
376
|
+
cfg.programName,
|
|
377
|
+
("It is not possible to add a pictures directory with a relative path if the project is not already saved"),
|
|
378
|
+
)
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
fd = QFileDialog()
|
|
382
|
+
fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
|
|
383
|
+
|
|
384
|
+
dir_path = fd.getExistingDirectory(None, "Select directory")
|
|
385
|
+
|
|
386
|
+
if not dir_path:
|
|
387
|
+
return
|
|
388
|
+
|
|
243
389
|
result = util.dir_images_number(dir_path)
|
|
244
390
|
if not result.get("number of images", 0):
|
|
245
391
|
response = dialog.MessageDialog(
|
|
@@ -250,7 +396,25 @@ class Observation(QDialog, Ui_Form):
|
|
|
250
396
|
if response == "Cancel":
|
|
251
397
|
return
|
|
252
398
|
|
|
253
|
-
|
|
399
|
+
# store directory for next usage
|
|
400
|
+
self.mem_dir = str(pl.Path(dir_path))
|
|
401
|
+
|
|
402
|
+
if " rel " in mode:
|
|
403
|
+
try:
|
|
404
|
+
pl.Path(dir_path).parent.relative_to(pl.Path(self.project_path).parent)
|
|
405
|
+
except ValueError:
|
|
406
|
+
QMessageBox.critical(
|
|
407
|
+
self,
|
|
408
|
+
cfg.programName,
|
|
409
|
+
f"The directory <b>{pl.Path(dir_path).parent}</b> is not contained in <b>{pl.Path(self.project_path).parent}</b>.",
|
|
410
|
+
)
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
if " rel " in mode:
|
|
414
|
+
# convert to relative path (relative to BORIS project file)
|
|
415
|
+
self.lw_images_directory.addItem(QListWidgetItem(str(pl.Path(dir_path).relative_to(pl.Path(self.project_path).parent))))
|
|
416
|
+
else:
|
|
417
|
+
self.lw_images_directory.addItem(QListWidgetItem(dir_path))
|
|
254
418
|
self.lb_images_info.setText(f"Number of images in {dir_path}: {result.get('number of images', 0)}")
|
|
255
419
|
|
|
256
420
|
def remove_images_directory(self):
|
|
@@ -290,10 +454,14 @@ class Observation(QDialog, Ui_Form):
|
|
|
290
454
|
"""
|
|
291
455
|
|
|
292
456
|
if self.cb_observation_time_interval.isChecked():
|
|
293
|
-
time_interval_dialog = dialog.Ask_time(
|
|
457
|
+
time_interval_dialog = dialog.Ask_time(0)
|
|
458
|
+
if self.time_format == cfg.S:
|
|
459
|
+
time_interval_dialog.time_widget.rb_seconds.setChecked(True)
|
|
460
|
+
if self.time_format == cfg.HHMMSS:
|
|
461
|
+
time_interval_dialog.time_widget.rb_time.setChecked(True)
|
|
294
462
|
time_interval_dialog.time_widget.set_time(0)
|
|
295
463
|
time_interval_dialog.setWindowTitle("Start observation at")
|
|
296
|
-
time_interval_dialog.label.setText("Start observation at")
|
|
464
|
+
time_interval_dialog.label.setText("<b>Start</b> observation at")
|
|
297
465
|
start_time, stop_time = 0, 0
|
|
298
466
|
if time_interval_dialog.exec_():
|
|
299
467
|
start_time = time_interval_dialog.time_widget.get_time()
|
|
@@ -302,7 +470,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
302
470
|
return
|
|
303
471
|
time_interval_dialog.time_widget.set_time(0)
|
|
304
472
|
time_interval_dialog.setWindowTitle("Stop observation at")
|
|
305
|
-
time_interval_dialog.label.setText("Stop observation at")
|
|
473
|
+
time_interval_dialog.label.setText("<b>Stop</b> observation at")
|
|
306
474
|
if time_interval_dialog.exec_():
|
|
307
475
|
stop_time = time_interval_dialog.time_widget.get_time()
|
|
308
476
|
else:
|
|
@@ -316,7 +484,10 @@ class Observation(QDialog, Ui_Form):
|
|
|
316
484
|
return
|
|
317
485
|
self.observation_time_interval = [start_time, stop_time]
|
|
318
486
|
self.cb_observation_time_interval.setText(
|
|
319
|
-
|
|
487
|
+
(
|
|
488
|
+
"Limit observation to a time interval: "
|
|
489
|
+
f"{util.smart_time_format(start_time, self.time_format)} - {util.smart_time_format(stop_time, self.time_format)}"
|
|
490
|
+
)
|
|
320
491
|
)
|
|
321
492
|
else:
|
|
322
493
|
self.observation_time_interval = [0, 0]
|
|
@@ -338,9 +509,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
338
509
|
|
|
339
510
|
if w.exec_():
|
|
340
511
|
d = {}
|
|
341
|
-
for col_idx, cb in zip(
|
|
342
|
-
self.tw_data_files.item(row, cfg.PLOT_DATA_COLUMNS_IDX).text().split(","), w.cbb
|
|
343
|
-
):
|
|
512
|
+
for col_idx, cb in zip(self.tw_data_files.item(row, cfg.PLOT_DATA_COLUMNS_IDX).text().split(","), w.cbb):
|
|
344
513
|
if cb.currentText() != "None":
|
|
345
514
|
d[col_idx] = cb.currentText()
|
|
346
515
|
self.tw_data_files.item(row, cfg.PLOT_DATA_CONVERTERS_IDX).setText(str(d))
|
|
@@ -361,7 +530,6 @@ class Observation(QDialog, Ui_Form):
|
|
|
361
530
|
return
|
|
362
531
|
|
|
363
532
|
if self.tw_data_files.selectedIndexes() or self.tw_data_files.rowCount() == 1:
|
|
364
|
-
|
|
365
533
|
if self.tw_data_files.rowCount() == 1:
|
|
366
534
|
row_idx = 0
|
|
367
535
|
else:
|
|
@@ -381,9 +549,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
381
549
|
time_interval = int(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_TIMEINTERVAL_IDX).text())
|
|
382
550
|
time_offset = int(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_TIMEOFFSET_IDX).text())
|
|
383
551
|
|
|
384
|
-
substract_first_value = self.tw_data_files.cellWidget(
|
|
385
|
-
row_idx, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX
|
|
386
|
-
).currentText()
|
|
552
|
+
substract_first_value = self.tw_data_files.cellWidget(row_idx, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX).currentText()
|
|
387
553
|
|
|
388
554
|
plot_color = self.tw_data_files.cellWidget(row_idx, cfg.PLOT_DATA_PLOTCOLOR_IDX).currentText()
|
|
389
555
|
|
|
@@ -428,6 +594,13 @@ class Observation(QDialog, Ui_Form):
|
|
|
428
594
|
else:
|
|
429
595
|
QMessageBox.warning(self, cfg.programName, "Select a data file")
|
|
430
596
|
|
|
597
|
+
def not_editable_column_color(self):
|
|
598
|
+
"""
|
|
599
|
+
return a color for the not editable column
|
|
600
|
+
"""
|
|
601
|
+
window_color = QApplication.instance().palette().window().color()
|
|
602
|
+
return QColor(window_color.red() - 5, window_color.green() - 5, window_color.blue() - 5)
|
|
603
|
+
|
|
431
604
|
def add_data_file(self, mode: str):
|
|
432
605
|
"""
|
|
433
606
|
user select a data file to be plotted synchronously with media file
|
|
@@ -461,19 +634,14 @@ class Observation(QDialog, Ui_Form):
|
|
|
461
634
|
QMessageBox.warning(
|
|
462
635
|
self,
|
|
463
636
|
cfg.programName,
|
|
464
|
-
(
|
|
465
|
-
"It is not yet possible to plot more than 2 external data sources"
|
|
466
|
-
"This limitation will be removed in future"
|
|
467
|
-
),
|
|
637
|
+
("It is not yet possible to plot more than 2 external data sourcesThis limitation will be removed in future"),
|
|
468
638
|
)
|
|
469
639
|
return
|
|
470
640
|
|
|
471
641
|
fd = QFileDialog()
|
|
472
642
|
fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
|
|
473
643
|
|
|
474
|
-
|
|
475
|
-
file_name = fn[0] if type(fn) is tuple else fn
|
|
476
|
-
|
|
644
|
+
file_name, _ = fd.getOpenFileName(self, "Add data file", "", "All files (*)")
|
|
477
645
|
if not file_name:
|
|
478
646
|
return
|
|
479
647
|
|
|
@@ -490,16 +658,14 @@ class Observation(QDialog, Ui_Form):
|
|
|
490
658
|
QMessageBox.critical(self, cfg.programName, "This file does not contain a constant number of columns")
|
|
491
659
|
return
|
|
492
660
|
|
|
493
|
-
header, footer = util.return_file_header_footer(
|
|
494
|
-
file_name, file_row_number=file_parameters["rows number"], row_number=5
|
|
495
|
-
)
|
|
661
|
+
header, footer = util.return_file_header_footer(file_name, file_row_number=file_parameters["rows number"], row_number=5)
|
|
496
662
|
|
|
497
663
|
if not header:
|
|
498
664
|
QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(file_name).name}")
|
|
499
665
|
return
|
|
500
666
|
|
|
501
667
|
w = dialog.View_data()
|
|
502
|
-
w.setWindowTitle(
|
|
668
|
+
w.setWindowTitle("View data")
|
|
503
669
|
w.lb.setText(f"View first and last rows of <b>{pl.Path(file_name).name}</b> file")
|
|
504
670
|
|
|
505
671
|
w.tw.setColumnCount(file_parameters["fields number"])
|
|
@@ -518,9 +684,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
518
684
|
|
|
519
685
|
# stats
|
|
520
686
|
try:
|
|
521
|
-
df = pd.read_csv(
|
|
522
|
-
file_name, sep=file_parameters["separator"], header=None if not file_parameters["has header"] else [0]
|
|
523
|
-
)
|
|
687
|
+
df = pd.read_csv(file_name, sep=file_parameters["separator"], header=None if not file_parameters["has header"] else [0])
|
|
524
688
|
# set columns names to based 1 index
|
|
525
689
|
if not file_parameters["has header"]:
|
|
526
690
|
df.columns = range(1, len(df.columns) + 1)
|
|
@@ -556,7 +720,6 @@ class Observation(QDialog, Ui_Form):
|
|
|
556
720
|
self.tw_data_files.setRowCount(self.tw_data_files.rowCount() + 1)
|
|
557
721
|
|
|
558
722
|
if " rel " in mode:
|
|
559
|
-
|
|
560
723
|
try:
|
|
561
724
|
file_path = str(pl.Path(file_name).relative_to(pl.Path(self.project_path).parent))
|
|
562
725
|
except ValueError:
|
|
@@ -585,15 +748,14 @@ class Observation(QDialog, Ui_Form):
|
|
|
585
748
|
item = QTableWidgetItem(value)
|
|
586
749
|
if col_idx == cfg.PLOT_DATA_CONVERTERS_IDX:
|
|
587
750
|
item.setFlags(Qt.ItemIsEnabled)
|
|
588
|
-
item.setBackground(QColor(230, 230, 230))
|
|
751
|
+
# item.setBackground(QColor(230, 230, 230))
|
|
752
|
+
item.setBackground(self.not_editable_column_color())
|
|
589
753
|
self.tw_data_files.setItem(self.tw_data_files.rowCount() - 1, col_idx, item)
|
|
590
754
|
|
|
591
755
|
# substract first value
|
|
592
756
|
combobox = QComboBox()
|
|
593
757
|
combobox.addItems(["True", "False"])
|
|
594
|
-
self.tw_data_files.setCellWidget(
|
|
595
|
-
self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX, combobox
|
|
596
|
-
)
|
|
758
|
+
self.tw_data_files.setCellWidget(self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX, combobox)
|
|
597
759
|
|
|
598
760
|
# plot line color
|
|
599
761
|
combobox = QComboBox()
|
|
@@ -622,18 +784,16 @@ class Observation(QDialog, Ui_Form):
|
|
|
622
784
|
if "error" in file_parameters:
|
|
623
785
|
QMessageBox.critical(self, cfg.programName, f"Error on file {data_file_path}: {file_parameters['error']}")
|
|
624
786
|
return
|
|
625
|
-
header, footer = util.return_file_header_footer(
|
|
626
|
-
data_file_path, file_row_number=file_parameters["rows number"], row_number=5
|
|
627
|
-
)
|
|
787
|
+
header, footer = util.return_file_header_footer(data_file_path, file_row_number=file_parameters["rows number"], row_number=5)
|
|
628
788
|
|
|
629
789
|
if not header:
|
|
630
790
|
QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(data_file_path).name}")
|
|
631
791
|
return
|
|
632
792
|
|
|
633
793
|
w = dialog.View_data()
|
|
634
|
-
w.setWindowTitle(
|
|
794
|
+
w.setWindowTitle("View data")
|
|
635
795
|
w.lb.setText(f"View first and last rows of <b>{pl.Path(data_file_path).name}</b> file")
|
|
636
|
-
w.pbOK.setText(
|
|
796
|
+
w.pbOK.setText(cfg.CLOSE)
|
|
637
797
|
w.label.setText("Index of columns to plot")
|
|
638
798
|
w.le.setEnabled(False)
|
|
639
799
|
w.le.setText(columns_to_plot)
|
|
@@ -676,73 +836,139 @@ class Observation(QDialog, Ui_Form):
|
|
|
676
836
|
extract wav of all media files loaded in player #1
|
|
677
837
|
"""
|
|
678
838
|
|
|
679
|
-
if self.cbVisualizeSpectrogram.isChecked()
|
|
680
|
-
|
|
681
|
-
# check if player 1 is selected
|
|
682
|
-
flag_player1 = False
|
|
683
|
-
for row in range(self.twVideo1.rowCount()):
|
|
684
|
-
if self.twVideo1.cellWidget(row, 0).currentText() == "1":
|
|
685
|
-
flag_player1 = True
|
|
839
|
+
if not self.cbVisualizeSpectrogram.isChecked() and not self.cb_visualize_waveform.isChecked():
|
|
840
|
+
return
|
|
686
841
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if dialog.MessageDialog(programName, ("You choose to visualize the spectrogram or waveform for the media in player #1.<br>"
|
|
694
|
-
"The WAV will be extracted from the media files, be patient"), [YES, NO]) == YES:
|
|
695
|
-
"""
|
|
696
|
-
if True:
|
|
842
|
+
flag_wav_produced = False
|
|
843
|
+
# check if player 1 is selected
|
|
844
|
+
flag_player1 = False
|
|
845
|
+
for row in range(self.twVideo1.rowCount()):
|
|
846
|
+
if self.twVideo1.cellWidget(row, 0).currentText() == "1":
|
|
847
|
+
flag_player1 = True
|
|
697
848
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
849
|
+
if not flag_player1:
|
|
850
|
+
QMessageBox.critical(self, cfg.programName, "The player #1 is not selected")
|
|
851
|
+
self.cbVisualizeSpectrogram.setChecked(False)
|
|
852
|
+
self.cb_visualize_waveform.setChecked(False)
|
|
853
|
+
return
|
|
703
854
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
855
|
+
if True:
|
|
856
|
+
w = dialog.Info_widget()
|
|
857
|
+
w.resize(350, 100)
|
|
858
|
+
# w.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
859
|
+
w.setWindowTitle("BORIS")
|
|
860
|
+
w.label.setText("Extracting WAV from media files...")
|
|
708
861
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
862
|
+
for row in range(self.twVideo1.rowCount()):
|
|
863
|
+
# check if player 1
|
|
864
|
+
if self.twVideo1.cellWidget(row, 0).currentText() != "1":
|
|
865
|
+
continue
|
|
866
|
+
|
|
867
|
+
media_file_path = project_functions.full_path(self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text(), self.project_path)
|
|
868
|
+
if self.twVideo1.item(row, cfg.HAS_AUDIO_IDX).text() == "False":
|
|
869
|
+
QMessageBox.critical(self, cfg.programName, f"The media file {media_file_path} does not seem to have audio")
|
|
870
|
+
flag_wav_produced = False
|
|
871
|
+
break
|
|
872
|
+
|
|
873
|
+
if os.path.isfile(media_file_path):
|
|
874
|
+
w.show()
|
|
875
|
+
QApplication.processEvents()
|
|
876
|
+
|
|
877
|
+
if util.extract_wav(self.ffmpeg_bin, media_file_path, self.tmp_dir) == "":
|
|
713
878
|
QMessageBox.critical(
|
|
714
|
-
self,
|
|
879
|
+
self,
|
|
880
|
+
cfg.programName,
|
|
881
|
+
f"Error during extracting WAV of the media file {media_file_path}",
|
|
715
882
|
)
|
|
716
883
|
flag_wav_produced = False
|
|
717
884
|
break
|
|
718
885
|
|
|
719
|
-
|
|
720
|
-
w.show()
|
|
721
|
-
QApplication.processEvents()
|
|
886
|
+
w.hide()
|
|
722
887
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
cfg.programName,
|
|
727
|
-
f"Error during extracting WAV of the media file {media_file_path}",
|
|
728
|
-
)
|
|
729
|
-
flag_wav_produced = False
|
|
730
|
-
break
|
|
888
|
+
flag_wav_produced = True
|
|
889
|
+
else:
|
|
890
|
+
QMessageBox.warning(self, cfg.programName, f"<b>{media_file_path}</b> file not found")
|
|
731
891
|
|
|
732
|
-
|
|
892
|
+
if not flag_wav_produced:
|
|
893
|
+
self.cbVisualizeSpectrogram.setChecked(False)
|
|
894
|
+
self.cb_visualize_waveform.setChecked(False)
|
|
733
895
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
896
|
+
def check_creation_date(self) -> int:
|
|
897
|
+
"""
|
|
898
|
+
check if media file exists
|
|
899
|
+
check if Creation Date tag is present in metadata of media file
|
|
737
900
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
901
|
+
Returns:
|
|
902
|
+
int: 0 if OK else error code: 1 -> media file date not used, 2 -> media file not found
|
|
903
|
+
|
|
904
|
+
"""
|
|
905
|
+
|
|
906
|
+
# check if media files exist
|
|
907
|
+
|
|
908
|
+
media_not_found_list: list = []
|
|
909
|
+
for row in range(self.twVideo1.rowCount()):
|
|
910
|
+
if not pl.Path(self.twVideo1.item(row, 2).text()).is_file():
|
|
911
|
+
media_not_found_list.append(self.twVideo1.item(row, 2).text())
|
|
912
|
+
|
|
913
|
+
"""
|
|
914
|
+
if media_list:
|
|
915
|
+
dlg = dialog.Results_dialog()
|
|
916
|
+
dlg.setWindowTitle("BORIS")
|
|
917
|
+
dlg.pbOK.setText("OK")
|
|
918
|
+
dlg.pbCancel.setVisible(False)
|
|
919
|
+
dlg.ptText.clear()
|
|
920
|
+
dlg.ptText.appendHtml(
|
|
921
|
+
(
|
|
922
|
+
"Some media file(s) were not found:<br>"
|
|
923
|
+
f"{'<br>'.join(media_list)}<br><br>"
|
|
924
|
+
"You cannot select the <b>Use the media creation date/time option</b>."
|
|
925
|
+
)
|
|
926
|
+
)
|
|
927
|
+
dlg.ptText.moveCursor(QTextCursor.Start)
|
|
928
|
+
ret = dlg.exec_()
|
|
929
|
+
"""
|
|
930
|
+
|
|
931
|
+
"""
|
|
932
|
+
not_tagged_media_list: list = []
|
|
933
|
+
for row in range(self.twVideo1.rowCount()):
|
|
934
|
+
if self.twVideo1.item(row, 2).text() not in media_not_found_list:
|
|
935
|
+
media_info = util.accurate_media_analysis(self.ffmpeg_bin, self.twVideo1.item(row, 2).text())
|
|
936
|
+
if cfg.MEDIA_CREATION_TIME not in media_info or media_info[cfg.MEDIA_CREATION_TIME] == cfg.NA:
|
|
937
|
+
not_tagged_media_list.append(self.twVideo1.item(row, 2).text())
|
|
938
|
+
else:
|
|
939
|
+
creation_time_epoch = int(dt.datetime.strptime(media_info[cfg.MEDIA_CREATION_TIME], "%Y-%m-%d %H:%M:%S").timestamp())
|
|
940
|
+
self.media_creation_time[self.twVideo1.item(row, 2).text()] = creation_time_epoch
|
|
941
|
+
|
|
942
|
+
if not_tagged_media_list:
|
|
943
|
+
dlg = dialog.Results_dialog()
|
|
944
|
+
dlg.setWindowTitle("BORIS")
|
|
945
|
+
dlg.pbOK.setText("Yes")
|
|
946
|
+
dlg.pbCancel.setVisible(True)
|
|
947
|
+
dlg.pbCancel.setText("No")
|
|
948
|
+
|
|
949
|
+
dlg.ptText.clear()
|
|
950
|
+
dlg.ptText.appendHtml(
|
|
951
|
+
(
|
|
952
|
+
"Some media file does not contain the <b>Creation date/time</b> metadata tag:<br>"
|
|
953
|
+
f"{'<br>'.join(not_tagged_media_list)}<br><br>"
|
|
954
|
+
"Use the media file date/time instead?"
|
|
955
|
+
)
|
|
956
|
+
)
|
|
957
|
+
dlg.ptText.moveCursor(QTextCursor.Start)
|
|
958
|
+
ret = dlg.exec_()
|
|
959
|
+
|
|
960
|
+
if ret == 1: # use file creation time
|
|
961
|
+
for media in not_tagged_media_list:
|
|
962
|
+
self.media_creation_time[media] = pl.Path(media).stat().st_ctime
|
|
963
|
+
return 0 # OK use media file creation date/time
|
|
742
964
|
else:
|
|
743
|
-
self.
|
|
744
|
-
self.
|
|
745
|
-
|
|
965
|
+
self.cb_media_creation_date_as_offset.setChecked(False)
|
|
966
|
+
self.media_creation_time = {}
|
|
967
|
+
return 1
|
|
968
|
+
else:
|
|
969
|
+
return 0 # OK all media have a 'creation time' tag
|
|
970
|
+
"""
|
|
971
|
+
return 0
|
|
746
972
|
|
|
747
973
|
def closeEvent(self, event):
|
|
748
974
|
"""
|
|
@@ -761,11 +987,13 @@ class Observation(QDialog, Ui_Form):
|
|
|
761
987
|
self.text = None
|
|
762
988
|
self.reject()
|
|
763
989
|
|
|
764
|
-
def check_parameters(self):
|
|
990
|
+
def check_parameters(self) -> bool:
|
|
765
991
|
"""
|
|
766
992
|
check observation parameters
|
|
767
993
|
|
|
768
|
-
|
|
994
|
+
Returns:
|
|
995
|
+
bool: True if everything is OK else False
|
|
996
|
+
|
|
769
997
|
"""
|
|
770
998
|
|
|
771
999
|
def is_numeric(s):
|
|
@@ -786,21 +1014,40 @@ class Observation(QDialog, Ui_Form):
|
|
|
786
1014
|
|
|
787
1015
|
# check if observation id not empty
|
|
788
1016
|
if not self.leObservationId.text():
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1017
|
+
QMessageBox.critical(
|
|
1018
|
+
self,
|
|
1019
|
+
cfg.programName,
|
|
1020
|
+
"The <b>observation id</b> is mandatory and must be unique.",
|
|
1021
|
+
)
|
|
793
1022
|
return False
|
|
794
1023
|
|
|
795
1024
|
# check if observation_type
|
|
796
1025
|
if not any((self.rb_media_files.isChecked(), self.rb_live.isChecked(), self.rb_images.isChecked())):
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1026
|
+
QMessageBox.critical(
|
|
1027
|
+
self,
|
|
1028
|
+
cfg.programName,
|
|
1029
|
+
"Choose an observation type.",
|
|
1030
|
+
)
|
|
801
1031
|
return False
|
|
802
1032
|
|
|
1033
|
+
# check if offset is correct
|
|
1034
|
+
if self.cb_time_offset.isChecked():
|
|
1035
|
+
if self.obs_time_offset.get_time() is None:
|
|
1036
|
+
QMessageBox.critical(
|
|
1037
|
+
self,
|
|
1038
|
+
cfg.programName,
|
|
1039
|
+
"Check the time offset value.",
|
|
1040
|
+
)
|
|
1041
|
+
return False
|
|
1042
|
+
|
|
803
1043
|
if self.rb_media_files.isChecked(): # observation based on media file(s)
|
|
1044
|
+
# check if media file exists
|
|
1045
|
+
media_file_not_found: list = []
|
|
1046
|
+
for row in range(self.twVideo1.rowCount()):
|
|
1047
|
+
# check if media file exists
|
|
1048
|
+
if not pl.Path(self.twVideo1.item(row, 2).text()).is_file():
|
|
1049
|
+
media_file_not_found.append(self.twVideo1.item(row, 2).text())
|
|
1050
|
+
|
|
804
1051
|
# check player number
|
|
805
1052
|
players_list: list = []
|
|
806
1053
|
players: dict = {} # for storing duration
|
|
@@ -813,18 +1060,20 @@ class Observation(QDialog, Ui_Form):
|
|
|
813
1060
|
|
|
814
1061
|
# check if player #1 is used
|
|
815
1062
|
if not players_list or min(players_list) > 1:
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1063
|
+
QMessageBox.critical(
|
|
1064
|
+
self,
|
|
1065
|
+
cfg.programName,
|
|
1066
|
+
"A media file must be loaded in player #1",
|
|
1067
|
+
)
|
|
820
1068
|
return False
|
|
821
1069
|
|
|
822
1070
|
# check if players are used in crescent order
|
|
823
1071
|
if set(list(range(min(players_list), max(players_list) + 1))) != set(players_list):
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1072
|
+
QMessageBox.critical(
|
|
1073
|
+
self,
|
|
1074
|
+
cfg.programName,
|
|
1075
|
+
"Some player are not used. Please reorganize your media files",
|
|
1076
|
+
)
|
|
828
1077
|
return False
|
|
829
1078
|
|
|
830
1079
|
# check if more media in player #1 and media in other players
|
|
@@ -856,7 +1105,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
856
1105
|
return False
|
|
857
1106
|
|
|
858
1107
|
# check that the longuest media is in player #1
|
|
859
|
-
durations = []
|
|
1108
|
+
durations: list = []
|
|
860
1109
|
for i in sorted(list(players.keys())):
|
|
861
1110
|
durations.append(sum(players[i]))
|
|
862
1111
|
if [x for x in durations[1:] if x > durations[0]]:
|
|
@@ -878,6 +1127,20 @@ class Observation(QDialog, Ui_Form):
|
|
|
878
1127
|
)
|
|
879
1128
|
return False
|
|
880
1129
|
|
|
1130
|
+
# check if offset set and only player #1 is used
|
|
1131
|
+
if len(set(players_list)) == 1:
|
|
1132
|
+
for row in range(self.twVideo1.rowCount()):
|
|
1133
|
+
if float(self.twVideo1.item(row, 1).text()):
|
|
1134
|
+
QMessageBox.critical(
|
|
1135
|
+
self,
|
|
1136
|
+
cfg.programName,
|
|
1137
|
+
(
|
|
1138
|
+
"It is not possible to use offset value(s) with only one player,<br>"
|
|
1139
|
+
"The offset values are use to synchronise various players."
|
|
1140
|
+
),
|
|
1141
|
+
)
|
|
1142
|
+
return False
|
|
1143
|
+
|
|
881
1144
|
# check offset for external data files
|
|
882
1145
|
for row in range(self.tw_data_files.rowCount()):
|
|
883
1146
|
if not is_numeric(self.tw_data_files.item(row, cfg.PLOT_DATA_TIMEOFFSET_IDX).text()):
|
|
@@ -893,6 +1156,18 @@ class Observation(QDialog, Ui_Form):
|
|
|
893
1156
|
)
|
|
894
1157
|
return False
|
|
895
1158
|
|
|
1159
|
+
# check media creation time tag in metadata
|
|
1160
|
+
# Disable because the check will be made at the observation start
|
|
1161
|
+
"""
|
|
1162
|
+
if self.cb_media_creation_date_as_offset.isChecked():
|
|
1163
|
+
if self.check_creation_date():
|
|
1164
|
+
return False
|
|
1165
|
+
"""
|
|
1166
|
+
|
|
1167
|
+
# check media creation date time (if option enabled)
|
|
1168
|
+
if self.check_media_creation_date():
|
|
1169
|
+
return False
|
|
1170
|
+
|
|
896
1171
|
if self.rb_images.isChecked(): # observation based on images directory
|
|
897
1172
|
if not self.lw_images_directory.count():
|
|
898
1173
|
QMessageBox.critical(self, cfg.programName, "You have to select at least one images directory")
|
|
@@ -901,9 +1176,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
901
1176
|
# check if indep variables are correct type
|
|
902
1177
|
for row in range(self.twIndepVariables.rowCount()):
|
|
903
1178
|
if self.twIndepVariables.item(row, 1).text() == cfg.NUMERIC:
|
|
904
|
-
if self.twIndepVariables.item(row, 2).text() and not is_numeric(
|
|
905
|
-
self.twIndepVariables.item(row, 2).text()
|
|
906
|
-
):
|
|
1179
|
+
if self.twIndepVariables.item(row, 2).text() and not is_numeric(self.twIndepVariables.item(row, 2).text()):
|
|
907
1180
|
QMessageBox.critical(
|
|
908
1181
|
self,
|
|
909
1182
|
cfg.programName,
|
|
@@ -925,11 +1198,10 @@ class Observation(QDialog, Ui_Form):
|
|
|
925
1198
|
)
|
|
926
1199
|
return False
|
|
927
1200
|
|
|
1201
|
+
# check if numeric indep variable values are numeric
|
|
928
1202
|
for row in range(self.twIndepVariables.rowCount()):
|
|
929
1203
|
if self.twIndepVariables.item(row, 1).text() == cfg.NUMERIC:
|
|
930
|
-
if self.twIndepVariables.item(row, 2).text() and not is_numeric(
|
|
931
|
-
self.twIndepVariables.item(row, 2).text()
|
|
932
|
-
):
|
|
1204
|
+
if self.twIndepVariables.item(row, 2).text() and not is_numeric(self.twIndepVariables.item(row, 2).text()):
|
|
933
1205
|
QMessageBox.critical(
|
|
934
1206
|
self,
|
|
935
1207
|
cfg.programName,
|
|
@@ -941,7 +1213,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
941
1213
|
|
|
942
1214
|
def pbLaunch_clicked(self):
|
|
943
1215
|
"""
|
|
944
|
-
Close
|
|
1216
|
+
Close dialog and start the observation
|
|
945
1217
|
"""
|
|
946
1218
|
|
|
947
1219
|
if self.check_parameters():
|
|
@@ -976,24 +1248,54 @@ class Observation(QDialog, Ui_Form):
|
|
|
976
1248
|
str: error message or empty string
|
|
977
1249
|
"""
|
|
978
1250
|
|
|
1251
|
+
logging.debug(f"check_media function for {file_path}")
|
|
1252
|
+
|
|
979
1253
|
media_info = util.accurate_media_analysis(self.ffmpeg_bin, file_path)
|
|
1254
|
+
|
|
1255
|
+
logging.debug(f"{media_info=}")
|
|
1256
|
+
|
|
980
1257
|
if "error" in media_info:
|
|
981
|
-
return
|
|
1258
|
+
return (True, media_info["error"])
|
|
1259
|
+
|
|
1260
|
+
if media_info["format_long_name"] == "Tele-typewriter":
|
|
1261
|
+
return (True, "Text file")
|
|
1262
|
+
|
|
1263
|
+
if media_info["duration"] > 0:
|
|
1264
|
+
if " rel " in mode:
|
|
1265
|
+
# convert to relative path (relative to BORIS project file)
|
|
1266
|
+
file_path = str(pl.Path(file_path).relative_to(pl.Path(self.project_path).parent))
|
|
1267
|
+
|
|
1268
|
+
self.mediaDurations[file_path] = float(media_info["duration"])
|
|
1269
|
+
elif media_info["has_video"] is False and media_info["audio_duration"]:
|
|
1270
|
+
self.mediaDurations[file_path] = float(media_info["audio_duration"])
|
|
982
1271
|
else:
|
|
983
|
-
|
|
1272
|
+
return (True, "Media duration not available")
|
|
984
1273
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1274
|
+
self.mediaFPS[file_path] = float(media_info["fps"])
|
|
1275
|
+
self.mediaHasVideo[file_path] = media_info["has_video"]
|
|
1276
|
+
self.mediaHasAudio[file_path] = media_info["has_audio"]
|
|
1277
|
+
|
|
1278
|
+
logging.debug(f"{file_path=}")
|
|
1279
|
+
|
|
1280
|
+
self.add_media_to_listview(file_path)
|
|
1281
|
+
return (False, "")
|
|
1282
|
+
|
|
1283
|
+
def update_media_options(self):
|
|
1284
|
+
"""
|
|
1285
|
+
update the media options
|
|
1286
|
+
"""
|
|
1287
|
+
for w in (
|
|
1288
|
+
self.cbVisualizeSpectrogram,
|
|
1289
|
+
self.cb_visualize_waveform,
|
|
1290
|
+
self.cb_observation_time_interval,
|
|
1291
|
+
self.cb_media_creation_date_as_offset,
|
|
1292
|
+
):
|
|
1293
|
+
w.setEnabled(self.twVideo1.rowCount() > 0)
|
|
1294
|
+
|
|
1295
|
+
# enable stop ongoing state events if n. media > 1
|
|
1296
|
+
self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(self.twVideo1.rowCount() > 0)
|
|
1297
|
+
|
|
1298
|
+
# self.creation_date_as_offset()
|
|
997
1299
|
|
|
998
1300
|
def add_media(self, mode: str):
|
|
999
1301
|
"""
|
|
@@ -1021,9 +1323,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
1021
1323
|
QMessageBox.critical(
|
|
1022
1324
|
self,
|
|
1023
1325
|
cfg.programName,
|
|
1024
|
-
(
|
|
1025
|
-
"It is not possible to add a media file without path or with a relative path if the project is not already saved"
|
|
1026
|
-
),
|
|
1326
|
+
("It is not possible to add a media file without path or with a relative path if the project is not already saved"),
|
|
1027
1327
|
)
|
|
1028
1328
|
return
|
|
1029
1329
|
|
|
@@ -1034,9 +1334,9 @@ class Observation(QDialog, Ui_Form):
|
|
|
1034
1334
|
fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
|
|
1035
1335
|
|
|
1036
1336
|
if "media " in mode:
|
|
1337
|
+
file_paths, _ = fd.getOpenFileNames(self, "Add media file(s)", "", "All files (*)")
|
|
1037
1338
|
|
|
1038
|
-
|
|
1039
|
-
file_paths = fn[0] if type(fn) is tuple else fn
|
|
1339
|
+
logging.debug(f"{file_paths=}")
|
|
1040
1340
|
|
|
1041
1341
|
if file_paths:
|
|
1042
1342
|
# store directory for next usage
|
|
@@ -1058,39 +1358,40 @@ class Observation(QDialog, Ui_Form):
|
|
|
1058
1358
|
if error:
|
|
1059
1359
|
QMessageBox.critical(self, cfg.programName, f"<b>{file_path}</b>. {msg}")
|
|
1060
1360
|
|
|
1061
|
-
if "dir " in mode:
|
|
1062
|
-
|
|
1361
|
+
if "dir " in mode: # add media from dir
|
|
1063
1362
|
dir_name = fd.getExistingDirectory(self, "Select directory")
|
|
1064
1363
|
if dir_name:
|
|
1065
1364
|
response = ""
|
|
1066
|
-
for file_path in
|
|
1067
|
-
|
|
1365
|
+
for file_path in sorted(pl.Path(dir_name).glob("*")):
|
|
1366
|
+
if not file_path.is_file():
|
|
1367
|
+
continue
|
|
1368
|
+
(error, msg) = self.check_media(str(file_path), mode)
|
|
1068
1369
|
if error:
|
|
1069
1370
|
if response != "Skip all non media files":
|
|
1070
1371
|
response = dialog.MessageDialog(
|
|
1071
1372
|
cfg.programName,
|
|
1072
1373
|
f"<b>{file_path}</b> {msg}",
|
|
1073
|
-
["Continue", "Skip all non media files",
|
|
1374
|
+
["Continue", "Skip all non media files", cfg.CANCEL],
|
|
1074
1375
|
)
|
|
1075
|
-
if response ==
|
|
1376
|
+
if response == cfg.CANCEL:
|
|
1076
1377
|
break
|
|
1378
|
+
# ask to use directory name / path as observation id
|
|
1379
|
+
if response != cfg.CANCEL:
|
|
1380
|
+
selected_obs_id = dialog.MessageDialog(
|
|
1381
|
+
cfg.programName,
|
|
1382
|
+
"Select the observation id",
|
|
1383
|
+
[dir_name, str(pl.Path(dir_name).name), cfg.CANCEL],
|
|
1384
|
+
)
|
|
1385
|
+
if selected_obs_id != cfg.CANCEL:
|
|
1386
|
+
self.leObservationId.setText(selected_obs_id)
|
|
1077
1387
|
|
|
1078
|
-
|
|
1079
|
-
self.cbVisualizeSpectrogram,
|
|
1080
|
-
self.cb_visualize_waveform,
|
|
1081
|
-
self.cb_observation_time_interval,
|
|
1082
|
-
self.cbCloseCurrentBehaviorsBetweenVideo,
|
|
1083
|
-
]:
|
|
1084
|
-
w.setEnabled(self.twVideo1.rowCount() > 0)
|
|
1085
|
-
|
|
1086
|
-
# disabled for problems
|
|
1087
|
-
self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
|
|
1388
|
+
self.update_media_options()
|
|
1088
1389
|
|
|
1089
1390
|
def add_media_to_listview(self, file_name):
|
|
1090
1391
|
"""
|
|
1091
1392
|
add media file path to list widget
|
|
1092
1393
|
"""
|
|
1093
|
-
|
|
1394
|
+
# add a row
|
|
1094
1395
|
self.twVideo1.setRowCount(self.twVideo1.rowCount() + 1)
|
|
1095
1396
|
|
|
1096
1397
|
for col_idx, s in enumerate(
|
|
@@ -1131,33 +1432,22 @@ class Observation(QDialog, Ui_Form):
|
|
|
1131
1432
|
remove all selected media files from list widget
|
|
1132
1433
|
"""
|
|
1133
1434
|
|
|
1134
|
-
if self.twVideo1.selectedIndexes():
|
|
1135
|
-
rows_to_delete = set([x.row() for x in self.twVideo1.selectedIndexes()])
|
|
1136
|
-
for row in sorted(rows_to_delete, reverse=True):
|
|
1137
|
-
media_path = self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text()
|
|
1138
|
-
self.twVideo1.removeRow(row)
|
|
1139
|
-
if media_path not in [
|
|
1140
|
-
self.twVideo1.item(idx, cfg.MEDIA_FILE_PATH_IDX).text() for idx in range(self.twVideo1.rowCount())
|
|
1141
|
-
]:
|
|
1142
|
-
try:
|
|
1143
|
-
del self.mediaDurations[media_path]
|
|
1144
|
-
except NameError:
|
|
1145
|
-
pass
|
|
1146
|
-
try:
|
|
1147
|
-
del self.mediaFPS[media_path]
|
|
1148
|
-
except NameError:
|
|
1149
|
-
pass
|
|
1150
|
-
|
|
1151
|
-
for w in [
|
|
1152
|
-
self.cbVisualizeSpectrogram,
|
|
1153
|
-
self.cb_visualize_waveform,
|
|
1154
|
-
self.cb_observation_time_interval,
|
|
1155
|
-
self.cbCloseCurrentBehaviorsBetweenVideo,
|
|
1156
|
-
]:
|
|
1157
|
-
w.setEnabled(self.twVideo1.rowCount() > 0)
|
|
1158
|
-
|
|
1159
|
-
# disabled for problems
|
|
1160
|
-
self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
|
|
1161
|
-
|
|
1162
|
-
else:
|
|
1435
|
+
if not self.twVideo1.selectedIndexes():
|
|
1163
1436
|
QMessageBox.warning(self, cfg.programName, "No media file selected")
|
|
1437
|
+
return
|
|
1438
|
+
|
|
1439
|
+
rows_to_delete = set([x.row() for x in self.twVideo1.selectedIndexes()])
|
|
1440
|
+
for row in sorted(rows_to_delete, reverse=True):
|
|
1441
|
+
media_path = self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text()
|
|
1442
|
+
self.twVideo1.removeRow(row)
|
|
1443
|
+
if media_path not in [self.twVideo1.item(idx, cfg.MEDIA_FILE_PATH_IDX).text() for idx in range(self.twVideo1.rowCount())]:
|
|
1444
|
+
try:
|
|
1445
|
+
del self.mediaDurations[media_path]
|
|
1446
|
+
except NameError:
|
|
1447
|
+
pass
|
|
1448
|
+
try:
|
|
1449
|
+
del self.mediaFPS[media_path]
|
|
1450
|
+
except NameError:
|
|
1451
|
+
pass
|
|
1452
|
+
|
|
1453
|
+
self.update_media_options()
|