boris-behav-obs 8.12__py3-none-any.whl → 9.7.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of boris-behav-obs might be problematic. Click here for more details.
- boris/__init__.py +1 -1
- boris/__main__.py +1 -1
- boris/about.py +28 -39
- boris/add_modifier.py +122 -109
- boris/add_modifier_ui.py +239 -135
- boris/advanced_event_filtering.py +81 -45
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +228 -229
- 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 +42 -49
- boris/config.py +141 -65
- boris/config_file.py +58 -67
- boris/connections.py +107 -61
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2373 -1786
- boris/core_qrc.py +15895 -10743
- boris/core_ui.py +943 -798
- boris/db_functions.py +17 -42
- boris/dev.py +109 -8
- boris/dialog.py +482 -236
- boris/duration_widget.py +9 -14
- boris/edit_event.py +61 -31
- boris/edit_event_ui.py +208 -97
- boris/event_operations.py +408 -293
- boris/events_cursor.py +25 -17
- boris/events_snapshots.py +36 -82
- boris/exclusion_matrix.py +4 -9
- boris/export_events.py +184 -223
- boris/export_observation.py +74 -100
- boris/external_processes.py +123 -98
- boris/geometric_measurement.py +644 -290
- 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 +17 -6
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +7 -9
- boris/mpv.py +1 -0
- boris/mpv2.py +732 -705
- boris/observation.py +533 -221
- boris/observation_operations.py +1025 -390
- boris/observation_ui.py +572 -362
- boris/observations_list.py +71 -53
- boris/otx_parser.py +74 -68
- boris/param_panel.py +31 -16
- boris/param_panel_ui.py +254 -138
- boris/player_dock_widget.py +90 -60
- boris/plot_data_module.py +25 -33
- boris/plot_events.py +127 -90
- boris/plot_events_rt.py +17 -31
- boris/plot_spectrogram_rt.py +95 -30
- boris/plot_waveform_rt.py +32 -21
- boris/plugins.py +431 -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 +306 -83
- boris/preferences_ui.py +684 -227
- boris/project.py +448 -293
- boris/project_functions.py +671 -238
- boris/project_import_export.py +213 -222
- boris/project_ui.py +674 -438
- boris/qrc_boris.py +6 -3
- boris/qrc_boris5.py +6 -3
- boris/select_modifiers.py +74 -48
- boris/select_observations.py +20 -198
- boris/select_subj_behav.py +67 -39
- boris/state_events.py +52 -35
- boris/subjects_pad.py +6 -9
- boris/synthetic_time_budget.py +45 -28
- boris/time_budget_functions.py +171 -171
- boris/time_budget_widget.py +84 -114
- boris/transitions.py +41 -47
- boris/utilities.py +627 -236
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +95 -29
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
- {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.6.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 -36
- boris/core.ui +0 -1556
- boris/edit_event.ui +0 -233
- boris/icons/logo_eye.ico +0 -0
- boris/map_creator.py +0 -850
- boris/observation.ui +0 -814
- boris/param_panel.ui +0 -379
- boris/preferences.ui +0 -537
- boris/project.ui +0 -1069
- boris/project_server.py +0 -236
- boris/vlc.py +0 -10343
- boris/vlc_local.py +0 -90
- boris_behav_obs-8.12.dist-info/LICENSE.TXT +0 -674
- boris_behav_obs-8.12.dist-info/METADATA +0 -128
- boris_behav_obs-8.12.dist-info/RECORD +0 -108
- boris_behav_obs-8.12.dist-info/entry_points.txt +0 -3
- {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
- {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.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,14 +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
|
|
25
|
+
import pandas as pd
|
|
26
26
|
import pathlib as pl
|
|
27
27
|
|
|
28
|
-
from
|
|
29
|
-
from
|
|
30
|
-
from
|
|
28
|
+
from PySide6.QtCore import Qt
|
|
29
|
+
from PySide6.QtGui import QColor
|
|
30
|
+
from PySide6.QtWidgets import (
|
|
31
31
|
QDialog,
|
|
32
32
|
QVBoxLayout,
|
|
33
33
|
QHBoxLayout,
|
|
@@ -42,10 +42,11 @@ from PyQt5.QtWidgets import (
|
|
|
42
42
|
QApplication,
|
|
43
43
|
QMenu,
|
|
44
44
|
QListWidgetItem,
|
|
45
|
+
QHeaderView,
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
from . import config as cfg
|
|
48
|
-
from . import dialog,
|
|
49
|
+
from . import dialog, plot_data_module, project_functions
|
|
49
50
|
from . import utilities as util
|
|
50
51
|
from . import gui_utilities
|
|
51
52
|
from .observation_ui import Ui_Form
|
|
@@ -75,7 +76,10 @@ class AssignConverter(QDialog):
|
|
|
75
76
|
self.cbb[-1].addItems(["None"] + sorted(converters.keys()))
|
|
76
77
|
|
|
77
78
|
if column_idx in col_conv:
|
|
78
|
-
|
|
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)
|
|
79
83
|
else:
|
|
80
84
|
self.cbb[-1].setCurrentIndex(0)
|
|
81
85
|
hbox.addWidget(self.cbb[-1])
|
|
@@ -96,7 +100,7 @@ class AssignConverter(QDialog):
|
|
|
96
100
|
|
|
97
101
|
|
|
98
102
|
class Observation(QDialog, Ui_Form):
|
|
99
|
-
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):
|
|
100
104
|
"""
|
|
101
105
|
Args:
|
|
102
106
|
tmp_dir (str): path of temporary directory
|
|
@@ -110,22 +114,31 @@ class Observation(QDialog, Ui_Form):
|
|
|
110
114
|
self.project_path = project_path
|
|
111
115
|
self.converters = converters
|
|
112
116
|
self.time_format = time_format
|
|
113
|
-
self.observation_time_interval = [0, 0]
|
|
117
|
+
self.observation_time_interval: tuple = [0, 0]
|
|
114
118
|
self.mem_dir = ""
|
|
115
119
|
self.test = None
|
|
116
120
|
|
|
117
121
|
self.setupUi(self)
|
|
118
122
|
|
|
119
123
|
# insert duration widget for time offset
|
|
120
|
-
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)
|
|
121
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)"""
|
|
122
133
|
|
|
123
134
|
# observation type
|
|
124
135
|
self.rb_media_files.toggled.connect(self.obs_type_changed)
|
|
125
136
|
self.rb_live.toggled.connect(self.obs_type_changed)
|
|
126
137
|
self.rb_images.toggled.connect(self.obs_type_changed)
|
|
127
138
|
|
|
128
|
-
|
|
139
|
+
# button menu for media
|
|
140
|
+
|
|
141
|
+
add_media_menu_items = [
|
|
129
142
|
"media abs path|with absolute path",
|
|
130
143
|
"media rel path|with relative path",
|
|
131
144
|
{
|
|
@@ -135,26 +148,71 @@ class Observation(QDialog, Ui_Form):
|
|
|
135
148
|
]
|
|
136
149
|
},
|
|
137
150
|
]
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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)
|
|
142
175
|
|
|
143
176
|
self.pbRemoveVideo.clicked.connect(self.remove_media)
|
|
144
177
|
|
|
145
|
-
#
|
|
178
|
+
# button menu for data file
|
|
146
179
|
data_menu_items = [
|
|
147
180
|
"data abs path|with absolute path",
|
|
148
181
|
"data rel path|with relative path",
|
|
149
182
|
]
|
|
150
183
|
|
|
151
|
-
menu_data = QMenu()
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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)
|
|
155
213
|
|
|
156
214
|
self.pb_remove_data_file.clicked.connect(self.remove_data_file)
|
|
157
|
-
self.pb_view_data_head.clicked.connect(self.
|
|
215
|
+
self.pb_view_data_head.clicked.connect(self.view_data_file_head_tail)
|
|
158
216
|
self.pb_plot_data.clicked.connect(self.plot_data_file)
|
|
159
217
|
|
|
160
218
|
self.pb_use_media_file_name_as_obsid.clicked.connect(self.use_media_file_name_as_obsid)
|
|
@@ -162,6 +220,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
162
220
|
|
|
163
221
|
self.cbVisualizeSpectrogram.clicked.connect(self.extract_wav)
|
|
164
222
|
self.cb_visualize_waveform.clicked.connect(self.extract_wav)
|
|
223
|
+
|
|
165
224
|
self.cb_observation_time_interval.clicked.connect(self.limit_time_interval)
|
|
166
225
|
|
|
167
226
|
self.pbSave.clicked.connect(self.pbSave_clicked)
|
|
@@ -169,21 +228,27 @@ class Observation(QDialog, Ui_Form):
|
|
|
169
228
|
self.pbCancel.clicked.connect(self.pbCancel_clicked)
|
|
170
229
|
|
|
171
230
|
self.tw_data_files.cellDoubleClicked[int, int].connect(self.tw_data_files_cellDoubleClicked)
|
|
231
|
+
self.tw_data_files.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
|
|
172
232
|
|
|
173
|
-
self.
|
|
233
|
+
self.twVideo1.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
|
|
174
234
|
|
|
175
|
-
self.
|
|
176
|
-
|
|
177
|
-
|
|
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)
|
|
178
245
|
|
|
179
|
-
|
|
180
|
-
self.cbCloseCurrentBehaviorsBetweenVideo.setChecked(False)
|
|
181
|
-
self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
|
|
246
|
+
self.cb_observation_time_interval.setEnabled(True)
|
|
182
247
|
|
|
183
248
|
self.cb_start_from_current_time.stateChanged.connect(self.cb_start_from_current_time_changed)
|
|
184
249
|
|
|
185
250
|
# images
|
|
186
|
-
self.pb_add_directory.clicked.connect(self.add_images_directory)
|
|
251
|
+
# self.pb_add_directory.clicked.connect(self.add_images_directory)
|
|
187
252
|
self.pb_remove_directory.clicked.connect(self.remove_images_directory)
|
|
188
253
|
|
|
189
254
|
self.tabWidget.setCurrentIndex(0)
|
|
@@ -191,6 +256,60 @@ class Observation(QDialog, Ui_Form):
|
|
|
191
256
|
# geometry
|
|
192
257
|
gui_utilities.restore_geometry(self, "new observation", (800, 650))
|
|
193
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
|
+
|
|
194
313
|
def use_media_file_name_as_obsid(self) -> None:
|
|
195
314
|
"""
|
|
196
315
|
set observation id with the media file name value (without path)
|
|
@@ -199,7 +318,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
199
318
|
QMessageBox.critical(self, cfg.programName, "A media file must be loaded in player #1")
|
|
200
319
|
return
|
|
201
320
|
|
|
202
|
-
first_media_file = ""
|
|
321
|
+
first_media_file: str = ""
|
|
203
322
|
for row in range(self.twVideo1.rowCount()):
|
|
204
323
|
if int(self.twVideo1.cellWidget(row, 0).currentText()) == 1:
|
|
205
324
|
first_media_file = self.twVideo1.item(row, 2).text()
|
|
@@ -234,11 +353,39 @@ class Observation(QDialog, Ui_Form):
|
|
|
234
353
|
# hide 'limit observation to time interval' for images
|
|
235
354
|
self.cb_observation_time_interval.setEnabled(not self.rb_images.isChecked())
|
|
236
355
|
|
|
237
|
-
def add_images_directory(self):
|
|
356
|
+
def add_images_directory(self, mode: str):
|
|
238
357
|
"""
|
|
239
358
|
add path to images directory
|
|
240
359
|
"""
|
|
241
|
-
|
|
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
|
+
|
|
242
389
|
result = util.dir_images_number(dir_path)
|
|
243
390
|
if not result.get("number of images", 0):
|
|
244
391
|
response = dialog.MessageDialog(
|
|
@@ -249,7 +396,25 @@ class Observation(QDialog, Ui_Form):
|
|
|
249
396
|
if response == "Cancel":
|
|
250
397
|
return
|
|
251
398
|
|
|
252
|
-
|
|
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))
|
|
253
418
|
self.lb_images_info.setText(f"Number of images in {dir_path}: {result.get('number of images', 0)}")
|
|
254
419
|
|
|
255
420
|
def remove_images_directory(self):
|
|
@@ -289,10 +454,14 @@ class Observation(QDialog, Ui_Form):
|
|
|
289
454
|
"""
|
|
290
455
|
|
|
291
456
|
if self.cb_observation_time_interval.isChecked():
|
|
292
|
-
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)
|
|
293
462
|
time_interval_dialog.time_widget.set_time(0)
|
|
294
463
|
time_interval_dialog.setWindowTitle("Start observation at")
|
|
295
|
-
time_interval_dialog.label.setText("Start observation at")
|
|
464
|
+
time_interval_dialog.label.setText("<b>Start</b> observation at")
|
|
296
465
|
start_time, stop_time = 0, 0
|
|
297
466
|
if time_interval_dialog.exec_():
|
|
298
467
|
start_time = time_interval_dialog.time_widget.get_time()
|
|
@@ -301,7 +470,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
301
470
|
return
|
|
302
471
|
time_interval_dialog.time_widget.set_time(0)
|
|
303
472
|
time_interval_dialog.setWindowTitle("Stop observation at")
|
|
304
|
-
time_interval_dialog.label.setText("Stop observation at")
|
|
473
|
+
time_interval_dialog.label.setText("<b>Stop</b> observation at")
|
|
305
474
|
if time_interval_dialog.exec_():
|
|
306
475
|
stop_time = time_interval_dialog.time_widget.get_time()
|
|
307
476
|
else:
|
|
@@ -315,7 +484,10 @@ class Observation(QDialog, Ui_Form):
|
|
|
315
484
|
return
|
|
316
485
|
self.observation_time_interval = [start_time, stop_time]
|
|
317
486
|
self.cb_observation_time_interval.setText(
|
|
318
|
-
|
|
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
|
+
)
|
|
319
491
|
)
|
|
320
492
|
else:
|
|
321
493
|
self.observation_time_interval = [0, 0]
|
|
@@ -337,9 +509,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
337
509
|
|
|
338
510
|
if w.exec_():
|
|
339
511
|
d = {}
|
|
340
|
-
for col_idx, cb in zip(
|
|
341
|
-
self.tw_data_files.item(row, cfg.PLOT_DATA_COLUMNS_IDX).text().split(","), w.cbb
|
|
342
|
-
):
|
|
512
|
+
for col_idx, cb in zip(self.tw_data_files.item(row, cfg.PLOT_DATA_COLUMNS_IDX).text().split(","), w.cbb):
|
|
343
513
|
if cb.currentText() != "None":
|
|
344
514
|
d[col_idx] = cb.currentText()
|
|
345
515
|
self.tw_data_files.item(row, cfg.PLOT_DATA_CONVERTERS_IDX).setText(str(d))
|
|
@@ -360,7 +530,6 @@ class Observation(QDialog, Ui_Form):
|
|
|
360
530
|
return
|
|
361
531
|
|
|
362
532
|
if self.tw_data_files.selectedIndexes() or self.tw_data_files.rowCount() == 1:
|
|
363
|
-
|
|
364
533
|
if self.tw_data_files.rowCount() == 1:
|
|
365
534
|
row_idx = 0
|
|
366
535
|
else:
|
|
@@ -380,9 +549,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
380
549
|
time_interval = int(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_TIMEINTERVAL_IDX).text())
|
|
381
550
|
time_offset = int(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_TIMEOFFSET_IDX).text())
|
|
382
551
|
|
|
383
|
-
substract_first_value = self.tw_data_files.cellWidget(
|
|
384
|
-
row_idx, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX
|
|
385
|
-
).currentText()
|
|
552
|
+
substract_first_value = self.tw_data_files.cellWidget(row_idx, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX).currentText()
|
|
386
553
|
|
|
387
554
|
plot_color = self.tw_data_files.cellWidget(row_idx, cfg.PLOT_DATA_PLOTCOLOR_IDX).currentText()
|
|
388
555
|
|
|
@@ -427,6 +594,13 @@ class Observation(QDialog, Ui_Form):
|
|
|
427
594
|
else:
|
|
428
595
|
QMessageBox.warning(self, cfg.programName, "Select a data file")
|
|
429
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
|
+
|
|
430
604
|
def add_data_file(self, mode: str):
|
|
431
605
|
"""
|
|
432
606
|
user select a data file to be plotted synchronously with media file
|
|
@@ -460,19 +634,14 @@ class Observation(QDialog, Ui_Form):
|
|
|
460
634
|
QMessageBox.warning(
|
|
461
635
|
self,
|
|
462
636
|
cfg.programName,
|
|
463
|
-
(
|
|
464
|
-
"It is not yet possible to plot more than 2 external data sources"
|
|
465
|
-
"This limitation will be removed in future"
|
|
466
|
-
),
|
|
637
|
+
("It is not yet possible to plot more than 2 external data sourcesThis limitation will be removed in future"),
|
|
467
638
|
)
|
|
468
639
|
return
|
|
469
640
|
|
|
470
641
|
fd = QFileDialog()
|
|
471
642
|
fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
|
|
472
643
|
|
|
473
|
-
|
|
474
|
-
file_name = fn[0] if type(fn) is tuple else fn
|
|
475
|
-
|
|
644
|
+
file_name, _ = fd.getOpenFileName(self, "Add data file", "", "All files (*)")
|
|
476
645
|
if not file_name:
|
|
477
646
|
return
|
|
478
647
|
|
|
@@ -489,18 +658,15 @@ class Observation(QDialog, Ui_Form):
|
|
|
489
658
|
QMessageBox.critical(self, cfg.programName, "This file does not contain a constant number of columns")
|
|
490
659
|
return
|
|
491
660
|
|
|
492
|
-
header, footer = util.return_file_header_footer(
|
|
493
|
-
file_name, file_row_number=file_parameters["rows number"], row_number=5
|
|
494
|
-
)
|
|
661
|
+
header, footer = util.return_file_header_footer(file_name, file_row_number=file_parameters["rows number"], row_number=5)
|
|
495
662
|
|
|
496
663
|
if not header:
|
|
497
664
|
QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(file_name).name}")
|
|
498
665
|
return
|
|
499
666
|
|
|
500
|
-
w = dialog.
|
|
501
|
-
w.setWindowTitle(
|
|
667
|
+
w = dialog.View_data()
|
|
668
|
+
w.setWindowTitle("View data")
|
|
502
669
|
w.lb.setText(f"View first and last rows of <b>{pl.Path(file_name).name}</b> file")
|
|
503
|
-
"""w.setWindowFlags(Qt.WindowStaysOnTopHint)"""
|
|
504
670
|
|
|
505
671
|
w.tw.setColumnCount(file_parameters["fields number"])
|
|
506
672
|
if footer:
|
|
@@ -516,13 +682,17 @@ class Observation(QDialog, Ui_Form):
|
|
|
516
682
|
item.setFlags(Qt.ItemIsEnabled)
|
|
517
683
|
w.tw.setItem(idx, col, item)
|
|
518
684
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
685
|
+
# stats
|
|
686
|
+
try:
|
|
687
|
+
df = pd.read_csv(file_name, sep=file_parameters["separator"], header=None if not file_parameters["has header"] else [0])
|
|
688
|
+
# set columns names to based 1 index
|
|
689
|
+
if not file_parameters["has header"]:
|
|
690
|
+
df.columns = range(1, len(df.columns) + 1)
|
|
691
|
+
|
|
692
|
+
stats_out = str(df.describe())
|
|
693
|
+
except Exception:
|
|
694
|
+
stats_out = "Not available"
|
|
695
|
+
w.stats.setPlainText(stats_out)
|
|
526
696
|
|
|
527
697
|
while True:
|
|
528
698
|
flag_ok = True
|
|
@@ -550,7 +720,6 @@ class Observation(QDialog, Ui_Form):
|
|
|
550
720
|
self.tw_data_files.setRowCount(self.tw_data_files.rowCount() + 1)
|
|
551
721
|
|
|
552
722
|
if " rel " in mode:
|
|
553
|
-
|
|
554
723
|
try:
|
|
555
724
|
file_path = str(pl.Path(file_name).relative_to(pl.Path(self.project_path).parent))
|
|
556
725
|
except ValueError:
|
|
@@ -579,22 +748,21 @@ class Observation(QDialog, Ui_Form):
|
|
|
579
748
|
item = QTableWidgetItem(value)
|
|
580
749
|
if col_idx == cfg.PLOT_DATA_CONVERTERS_IDX:
|
|
581
750
|
item.setFlags(Qt.ItemIsEnabled)
|
|
582
|
-
item.setBackground(QColor(230, 230, 230))
|
|
751
|
+
# item.setBackground(QColor(230, 230, 230))
|
|
752
|
+
item.setBackground(self.not_editable_column_color())
|
|
583
753
|
self.tw_data_files.setItem(self.tw_data_files.rowCount() - 1, col_idx, item)
|
|
584
754
|
|
|
585
755
|
# substract first value
|
|
586
756
|
combobox = QComboBox()
|
|
587
757
|
combobox.addItems(["True", "False"])
|
|
588
|
-
self.tw_data_files.setCellWidget(
|
|
589
|
-
self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX, combobox
|
|
590
|
-
)
|
|
758
|
+
self.tw_data_files.setCellWidget(self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX, combobox)
|
|
591
759
|
|
|
592
760
|
# plot line color
|
|
593
761
|
combobox = QComboBox()
|
|
594
762
|
combobox.addItems(cfg.DATA_PLOT_STYLES)
|
|
595
763
|
self.tw_data_files.setCellWidget(self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_PLOTCOLOR_IDX, combobox)
|
|
596
764
|
|
|
597
|
-
def
|
|
765
|
+
def view_data_file_head_tail(self) -> None:
|
|
598
766
|
"""
|
|
599
767
|
view first and last rows of data file
|
|
600
768
|
"""
|
|
@@ -605,7 +773,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
605
773
|
if self.tw_data_files.rowCount() == 1:
|
|
606
774
|
data_file_path = project_functions.full_path(self.tw_data_files.item(0, 0).text(), self.project_path)
|
|
607
775
|
columns_to_plot = self.tw_data_files.item(0, 1).text()
|
|
608
|
-
else:
|
|
776
|
+
else: # selected file
|
|
609
777
|
data_file_path = project_functions.full_path(
|
|
610
778
|
self.tw_data_files.item(self.tw_data_files.selectedIndexes()[0].row(), 0).text(), self.project_path
|
|
611
779
|
)
|
|
@@ -616,18 +784,16 @@ class Observation(QDialog, Ui_Form):
|
|
|
616
784
|
if "error" in file_parameters:
|
|
617
785
|
QMessageBox.critical(self, cfg.programName, f"Error on file {data_file_path}: {file_parameters['error']}")
|
|
618
786
|
return
|
|
619
|
-
header, footer = util.return_file_header_footer(
|
|
620
|
-
data_file_path, file_row_number=file_parameters["rows number"], row_number=5
|
|
621
|
-
)
|
|
787
|
+
header, footer = util.return_file_header_footer(data_file_path, file_row_number=file_parameters["rows number"], row_number=5)
|
|
622
788
|
|
|
623
789
|
if not header:
|
|
624
790
|
QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(data_file_path).name}")
|
|
625
791
|
return
|
|
626
792
|
|
|
627
|
-
w = dialog.
|
|
628
|
-
w.setWindowTitle(
|
|
793
|
+
w = dialog.View_data()
|
|
794
|
+
w.setWindowTitle("View data")
|
|
629
795
|
w.lb.setText(f"View first and last rows of <b>{pl.Path(data_file_path).name}</b> file")
|
|
630
|
-
w.pbOK.setText(
|
|
796
|
+
w.pbOK.setText(cfg.CLOSE)
|
|
631
797
|
w.label.setText("Index of columns to plot")
|
|
632
798
|
w.le.setEnabled(False)
|
|
633
799
|
w.le.setText(columns_to_plot)
|
|
@@ -647,6 +813,22 @@ class Observation(QDialog, Ui_Form):
|
|
|
647
813
|
item.setFlags(Qt.ItemIsEnabled)
|
|
648
814
|
w.tw.setItem(idx, col, item)
|
|
649
815
|
|
|
816
|
+
# stats
|
|
817
|
+
try:
|
|
818
|
+
df = pd.read_csv(
|
|
819
|
+
data_file_path,
|
|
820
|
+
sep=file_parameters["separator"],
|
|
821
|
+
header=None if not file_parameters["has header"] else [0],
|
|
822
|
+
)
|
|
823
|
+
# set columns names to based 1 index
|
|
824
|
+
if not file_parameters["has header"]:
|
|
825
|
+
df.columns = range(1, len(df.columns) + 1)
|
|
826
|
+
|
|
827
|
+
stats_out = str(df.describe())
|
|
828
|
+
except Exception:
|
|
829
|
+
stats_out = "Not available"
|
|
830
|
+
w.stats.setPlainText(stats_out)
|
|
831
|
+
|
|
650
832
|
w.exec_()
|
|
651
833
|
|
|
652
834
|
def extract_wav(self):
|
|
@@ -654,73 +836,139 @@ class Observation(QDialog, Ui_Form):
|
|
|
654
836
|
extract wav of all media files loaded in player #1
|
|
655
837
|
"""
|
|
656
838
|
|
|
657
|
-
if self.cbVisualizeSpectrogram.isChecked()
|
|
658
|
-
|
|
659
|
-
# check if player 1 is selected
|
|
660
|
-
flag_player1 = False
|
|
661
|
-
for row in range(self.twVideo1.rowCount()):
|
|
662
|
-
if self.twVideo1.cellWidget(row, 0).currentText() == "1":
|
|
663
|
-
flag_player1 = True
|
|
839
|
+
if not self.cbVisualizeSpectrogram.isChecked() and not self.cb_visualize_waveform.isChecked():
|
|
840
|
+
return
|
|
664
841
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
if dialog.MessageDialog(programName, ("You choose to visualize the spectrogram or waveform for the media in player #1.<br>"
|
|
672
|
-
"The WAV will be extracted from the media files, be patient"), [YES, NO]) == YES:
|
|
673
|
-
"""
|
|
674
|
-
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
|
|
675
848
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
|
681
854
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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...")
|
|
686
861
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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) == "":
|
|
691
878
|
QMessageBox.critical(
|
|
692
|
-
self,
|
|
879
|
+
self,
|
|
880
|
+
cfg.programName,
|
|
881
|
+
f"Error during extracting WAV of the media file {media_file_path}",
|
|
693
882
|
)
|
|
694
883
|
flag_wav_produced = False
|
|
695
884
|
break
|
|
696
885
|
|
|
697
|
-
|
|
698
|
-
w.show()
|
|
699
|
-
QApplication.processEvents()
|
|
886
|
+
w.hide()
|
|
700
887
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
cfg.programName,
|
|
705
|
-
f"Error during extracting WAV of the media file {media_file_path}",
|
|
706
|
-
)
|
|
707
|
-
flag_wav_produced = False
|
|
708
|
-
break
|
|
888
|
+
flag_wav_produced = True
|
|
889
|
+
else:
|
|
890
|
+
QMessageBox.warning(self, cfg.programName, f"<b>{media_file_path}</b> file not found")
|
|
709
891
|
|
|
710
|
-
|
|
892
|
+
if not flag_wav_produced:
|
|
893
|
+
self.cbVisualizeSpectrogram.setChecked(False)
|
|
894
|
+
self.cb_visualize_waveform.setChecked(False)
|
|
711
895
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
|
715
900
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
|
720
964
|
else:
|
|
721
|
-
self.
|
|
722
|
-
self.
|
|
723
|
-
|
|
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
|
|
724
972
|
|
|
725
973
|
def closeEvent(self, event):
|
|
726
974
|
"""
|
|
@@ -739,11 +987,13 @@ class Observation(QDialog, Ui_Form):
|
|
|
739
987
|
self.text = None
|
|
740
988
|
self.reject()
|
|
741
989
|
|
|
742
|
-
def check_parameters(self):
|
|
990
|
+
def check_parameters(self) -> bool:
|
|
743
991
|
"""
|
|
744
992
|
check observation parameters
|
|
745
993
|
|
|
746
|
-
|
|
994
|
+
Returns:
|
|
995
|
+
bool: True if everything is OK else False
|
|
996
|
+
|
|
747
997
|
"""
|
|
748
998
|
|
|
749
999
|
def is_numeric(s):
|
|
@@ -764,21 +1014,40 @@ class Observation(QDialog, Ui_Form):
|
|
|
764
1014
|
|
|
765
1015
|
# check if observation id not empty
|
|
766
1016
|
if not self.leObservationId.text():
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1017
|
+
QMessageBox.critical(
|
|
1018
|
+
self,
|
|
1019
|
+
cfg.programName,
|
|
1020
|
+
"The <b>observation id</b> is mandatory and must be unique.",
|
|
1021
|
+
)
|
|
771
1022
|
return False
|
|
772
1023
|
|
|
773
1024
|
# check if observation_type
|
|
774
1025
|
if not any((self.rb_media_files.isChecked(), self.rb_live.isChecked(), self.rb_images.isChecked())):
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1026
|
+
QMessageBox.critical(
|
|
1027
|
+
self,
|
|
1028
|
+
cfg.programName,
|
|
1029
|
+
"Choose an observation type.",
|
|
1030
|
+
)
|
|
779
1031
|
return False
|
|
780
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
|
+
|
|
781
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
|
+
|
|
782
1051
|
# check player number
|
|
783
1052
|
players_list: list = []
|
|
784
1053
|
players: dict = {} # for storing duration
|
|
@@ -791,18 +1060,20 @@ class Observation(QDialog, Ui_Form):
|
|
|
791
1060
|
|
|
792
1061
|
# check if player #1 is used
|
|
793
1062
|
if not players_list or min(players_list) > 1:
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1063
|
+
QMessageBox.critical(
|
|
1064
|
+
self,
|
|
1065
|
+
cfg.programName,
|
|
1066
|
+
"A media file must be loaded in player #1",
|
|
1067
|
+
)
|
|
798
1068
|
return False
|
|
799
1069
|
|
|
800
1070
|
# check if players are used in crescent order
|
|
801
1071
|
if set(list(range(min(players_list), max(players_list) + 1))) != set(players_list):
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1072
|
+
QMessageBox.critical(
|
|
1073
|
+
self,
|
|
1074
|
+
cfg.programName,
|
|
1075
|
+
"Some player are not used. Please reorganize your media files",
|
|
1076
|
+
)
|
|
806
1077
|
return False
|
|
807
1078
|
|
|
808
1079
|
# check if more media in player #1 and media in other players
|
|
@@ -834,7 +1105,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
834
1105
|
return False
|
|
835
1106
|
|
|
836
1107
|
# check that the longuest media is in player #1
|
|
837
|
-
durations = []
|
|
1108
|
+
durations: list = []
|
|
838
1109
|
for i in sorted(list(players.keys())):
|
|
839
1110
|
durations.append(sum(players[i]))
|
|
840
1111
|
if [x for x in durations[1:] if x > durations[0]]:
|
|
@@ -856,6 +1127,20 @@ class Observation(QDialog, Ui_Form):
|
|
|
856
1127
|
)
|
|
857
1128
|
return False
|
|
858
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
|
+
|
|
859
1144
|
# check offset for external data files
|
|
860
1145
|
for row in range(self.tw_data_files.rowCount()):
|
|
861
1146
|
if not is_numeric(self.tw_data_files.item(row, cfg.PLOT_DATA_TIMEOFFSET_IDX).text()):
|
|
@@ -871,6 +1156,18 @@ class Observation(QDialog, Ui_Form):
|
|
|
871
1156
|
)
|
|
872
1157
|
return False
|
|
873
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
|
+
|
|
874
1171
|
if self.rb_images.isChecked(): # observation based on images directory
|
|
875
1172
|
if not self.lw_images_directory.count():
|
|
876
1173
|
QMessageBox.critical(self, cfg.programName, "You have to select at least one images directory")
|
|
@@ -879,9 +1176,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
879
1176
|
# check if indep variables are correct type
|
|
880
1177
|
for row in range(self.twIndepVariables.rowCount()):
|
|
881
1178
|
if self.twIndepVariables.item(row, 1).text() == cfg.NUMERIC:
|
|
882
|
-
if self.twIndepVariables.item(row, 2).text() and not is_numeric(
|
|
883
|
-
self.twIndepVariables.item(row, 2).text()
|
|
884
|
-
):
|
|
1179
|
+
if self.twIndepVariables.item(row, 2).text() and not is_numeric(self.twIndepVariables.item(row, 2).text()):
|
|
885
1180
|
QMessageBox.critical(
|
|
886
1181
|
self,
|
|
887
1182
|
cfg.programName,
|
|
@@ -903,11 +1198,10 @@ class Observation(QDialog, Ui_Form):
|
|
|
903
1198
|
)
|
|
904
1199
|
return False
|
|
905
1200
|
|
|
1201
|
+
# check if numeric indep variable values are numeric
|
|
906
1202
|
for row in range(self.twIndepVariables.rowCount()):
|
|
907
1203
|
if self.twIndepVariables.item(row, 1).text() == cfg.NUMERIC:
|
|
908
|
-
if self.twIndepVariables.item(row, 2).text() and not is_numeric(
|
|
909
|
-
self.twIndepVariables.item(row, 2).text()
|
|
910
|
-
):
|
|
1204
|
+
if self.twIndepVariables.item(row, 2).text() and not is_numeric(self.twIndepVariables.item(row, 2).text()):
|
|
911
1205
|
QMessageBox.critical(
|
|
912
1206
|
self,
|
|
913
1207
|
cfg.programName,
|
|
@@ -919,7 +1213,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
919
1213
|
|
|
920
1214
|
def pbLaunch_clicked(self):
|
|
921
1215
|
"""
|
|
922
|
-
Close
|
|
1216
|
+
Close dialog and start the observation
|
|
923
1217
|
"""
|
|
924
1218
|
|
|
925
1219
|
if self.check_parameters():
|
|
@@ -954,24 +1248,54 @@ class Observation(QDialog, Ui_Form):
|
|
|
954
1248
|
str: error message or empty string
|
|
955
1249
|
"""
|
|
956
1250
|
|
|
1251
|
+
logging.debug(f"check_media function for {file_path}")
|
|
1252
|
+
|
|
957
1253
|
media_info = util.accurate_media_analysis(self.ffmpeg_bin, file_path)
|
|
1254
|
+
|
|
1255
|
+
logging.debug(f"{media_info=}")
|
|
1256
|
+
|
|
958
1257
|
if "error" in media_info:
|
|
959
|
-
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"])
|
|
960
1271
|
else:
|
|
961
|
-
|
|
1272
|
+
return (True, "Media duration not available")
|
|
962
1273
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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()
|
|
975
1299
|
|
|
976
1300
|
def add_media(self, mode: str):
|
|
977
1301
|
"""
|
|
@@ -999,9 +1323,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
999
1323
|
QMessageBox.critical(
|
|
1000
1324
|
self,
|
|
1001
1325
|
cfg.programName,
|
|
1002
|
-
(
|
|
1003
|
-
"It is not possible to add a media file without path or with a relative path if the project is not already saved"
|
|
1004
|
-
),
|
|
1326
|
+
("It is not possible to add a media file without path or with a relative path if the project is not already saved"),
|
|
1005
1327
|
)
|
|
1006
1328
|
return
|
|
1007
1329
|
|
|
@@ -1012,9 +1334,9 @@ class Observation(QDialog, Ui_Form):
|
|
|
1012
1334
|
fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
|
|
1013
1335
|
|
|
1014
1336
|
if "media " in mode:
|
|
1337
|
+
file_paths, _ = fd.getOpenFileNames(self, "Add media file(s)", "", "All files (*)")
|
|
1015
1338
|
|
|
1016
|
-
|
|
1017
|
-
file_paths = fn[0] if type(fn) is tuple else fn
|
|
1339
|
+
logging.debug(f"{file_paths=}")
|
|
1018
1340
|
|
|
1019
1341
|
if file_paths:
|
|
1020
1342
|
# store directory for next usage
|
|
@@ -1036,39 +1358,40 @@ class Observation(QDialog, Ui_Form):
|
|
|
1036
1358
|
if error:
|
|
1037
1359
|
QMessageBox.critical(self, cfg.programName, f"<b>{file_path}</b>. {msg}")
|
|
1038
1360
|
|
|
1039
|
-
if "dir " in mode:
|
|
1040
|
-
|
|
1361
|
+
if "dir " in mode: # add media from dir
|
|
1041
1362
|
dir_name = fd.getExistingDirectory(self, "Select directory")
|
|
1042
1363
|
if dir_name:
|
|
1043
1364
|
response = ""
|
|
1044
|
-
for file_path in
|
|
1045
|
-
|
|
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)
|
|
1046
1369
|
if error:
|
|
1047
1370
|
if response != "Skip all non media files":
|
|
1048
1371
|
response = dialog.MessageDialog(
|
|
1049
1372
|
cfg.programName,
|
|
1050
1373
|
f"<b>{file_path}</b> {msg}",
|
|
1051
|
-
["Continue", "Skip all non media files",
|
|
1374
|
+
["Continue", "Skip all non media files", cfg.CANCEL],
|
|
1052
1375
|
)
|
|
1053
|
-
if response ==
|
|
1376
|
+
if response == cfg.CANCEL:
|
|
1054
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)
|
|
1055
1387
|
|
|
1056
|
-
|
|
1057
|
-
self.cbVisualizeSpectrogram,
|
|
1058
|
-
self.cb_visualize_waveform,
|
|
1059
|
-
self.cb_observation_time_interval,
|
|
1060
|
-
self.cbCloseCurrentBehaviorsBetweenVideo,
|
|
1061
|
-
]:
|
|
1062
|
-
w.setEnabled(self.twVideo1.rowCount() > 0)
|
|
1063
|
-
|
|
1064
|
-
# disabled for problems
|
|
1065
|
-
self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
|
|
1388
|
+
self.update_media_options()
|
|
1066
1389
|
|
|
1067
1390
|
def add_media_to_listview(self, file_name):
|
|
1068
1391
|
"""
|
|
1069
1392
|
add media file path to list widget
|
|
1070
1393
|
"""
|
|
1071
|
-
|
|
1394
|
+
# add a row
|
|
1072
1395
|
self.twVideo1.setRowCount(self.twVideo1.rowCount() + 1)
|
|
1073
1396
|
|
|
1074
1397
|
for col_idx, s in enumerate(
|
|
@@ -1109,33 +1432,22 @@ class Observation(QDialog, Ui_Form):
|
|
|
1109
1432
|
remove all selected media files from list widget
|
|
1110
1433
|
"""
|
|
1111
1434
|
|
|
1112
|
-
if self.twVideo1.selectedIndexes():
|
|
1113
|
-
rows_to_delete = set([x.row() for x in self.twVideo1.selectedIndexes()])
|
|
1114
|
-
for row in sorted(rows_to_delete, reverse=True):
|
|
1115
|
-
media_path = self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text()
|
|
1116
|
-
self.twVideo1.removeRow(row)
|
|
1117
|
-
if media_path not in [
|
|
1118
|
-
self.twVideo1.item(idx, cfg.MEDIA_FILE_PATH_IDX).text() for idx in range(self.twVideo1.rowCount())
|
|
1119
|
-
]:
|
|
1120
|
-
try:
|
|
1121
|
-
del self.mediaDurations[media_path]
|
|
1122
|
-
except NameError:
|
|
1123
|
-
pass
|
|
1124
|
-
try:
|
|
1125
|
-
del self.mediaFPS[media_path]
|
|
1126
|
-
except NameError:
|
|
1127
|
-
pass
|
|
1128
|
-
|
|
1129
|
-
for w in [
|
|
1130
|
-
self.cbVisualizeSpectrogram,
|
|
1131
|
-
self.cb_visualize_waveform,
|
|
1132
|
-
self.cb_observation_time_interval,
|
|
1133
|
-
self.cbCloseCurrentBehaviorsBetweenVideo,
|
|
1134
|
-
]:
|
|
1135
|
-
w.setEnabled(self.twVideo1.rowCount() > 0)
|
|
1136
|
-
|
|
1137
|
-
# disabled for problems
|
|
1138
|
-
self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
|
|
1139
|
-
|
|
1140
|
-
else:
|
|
1435
|
+
if not self.twVideo1.selectedIndexes():
|
|
1141
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()
|