boris-behav-obs 8.16.6__py3-none-any.whl → 9.7.1__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 +24 -40
- boris/add_modifier.py +88 -80
- boris/add_modifier_ui.py +235 -131
- boris/advanced_event_filtering.py +23 -29
- 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 +16 -34
- boris/config.py +101 -49
- boris/config_file.py +55 -64
- boris/connections.py +105 -58
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2108 -1275
- boris/core_qrc.py +15892 -10829
- boris/core_ui.py +941 -806
- boris/db_functions.py +17 -42
- boris/dev.py +134 -0
- 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 +304 -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 +127 -36
- boris/observation.py +493 -210
- boris/observation_operations.py +1010 -391
- 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 +18 -53
- boris/plot_events.py +56 -153
- boris/plot_events_rt.py +16 -30
- boris/plot_spectrogram_rt.py +80 -56
- boris/plot_waveform_rt.py +23 -48
- 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 +298 -123
- boris/preferences_ui.py +664 -225
- boris/project.py +293 -270
- boris/project_functions.py +610 -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 +6 -8
- boris/synthetic_time_budget.py +25 -17
- boris/time_budget_functions.py +169 -169
- boris/time_budget_widget.py +71 -86
- boris/transitions.py +41 -41
- boris/utilities.py +562 -222
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +78 -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.1.dist-info/METADATA +140 -0
- boris_behav_obs-9.7.1.dist-info/RECORD +109 -0
- {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.1.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.1.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.6.dist-info/LICENSE.TXT +0 -674
- boris_behav_obs-8.16.6.dist-info/METADATA +0 -134
- boris_behav_obs-8.16.6.dist-info/RECORD +0 -106
- boris_behav_obs-8.16.6.dist-info/entry_points.txt +0 -2
- {boris → boris_behav_obs-9.7.1.dist-info/licenses}/LICENSE.TXT +0 -0
- {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.1.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,
|
|
@@ -46,7 +45,7 @@ from PyQt5.QtWidgets import (
|
|
|
46
45
|
)
|
|
47
46
|
|
|
48
47
|
from . import config as cfg
|
|
49
|
-
from . import dialog,
|
|
48
|
+
from . import dialog, plot_data_module, project_functions
|
|
50
49
|
from . import utilities as util
|
|
51
50
|
from . import gui_utilities
|
|
52
51
|
from .observation_ui import Ui_Form
|
|
@@ -97,7 +96,7 @@ class AssignConverter(QDialog):
|
|
|
97
96
|
|
|
98
97
|
|
|
99
98
|
class Observation(QDialog, Ui_Form):
|
|
100
|
-
def __init__(self, tmp_dir, project_path="", converters={}, time_format=cfg.S, parent=None):
|
|
99
|
+
def __init__(self, tmp_dir: str, project_path: str = "", converters: dict = {}, time_format: str = cfg.S, parent=None):
|
|
101
100
|
"""
|
|
102
101
|
Args:
|
|
103
102
|
tmp_dir (str): path of temporary directory
|
|
@@ -111,22 +110,31 @@ class Observation(QDialog, Ui_Form):
|
|
|
111
110
|
self.project_path = project_path
|
|
112
111
|
self.converters = converters
|
|
113
112
|
self.time_format = time_format
|
|
114
|
-
self.observation_time_interval = [0, 0]
|
|
113
|
+
self.observation_time_interval: tuple = [0, 0]
|
|
115
114
|
self.mem_dir = ""
|
|
116
115
|
self.test = None
|
|
117
116
|
|
|
118
117
|
self.setupUi(self)
|
|
119
118
|
|
|
120
119
|
# insert duration widget for time offset
|
|
121
|
-
self.obs_time_offset = duration_widget.Duration_widget(0)
|
|
120
|
+
# self.obs_time_offset = duration_widget.Duration_widget(0)
|
|
121
|
+
self.obs_time_offset = dialog.get_time_widget(0)
|
|
122
122
|
self.horizontalLayout_6.insertWidget(1, self.obs_time_offset)
|
|
123
|
+
self.obs_time_offset.setEnabled(False)
|
|
124
|
+
|
|
125
|
+
# time offset
|
|
126
|
+
self.cb_time_offset.stateChanged.connect(self.cb_time_offset_changed)
|
|
127
|
+
# date offset
|
|
128
|
+
"""self.cb_date_offset.stateChanged.connect(self.cb_date_offset_changed)"""
|
|
123
129
|
|
|
124
130
|
# observation type
|
|
125
131
|
self.rb_media_files.toggled.connect(self.obs_type_changed)
|
|
126
132
|
self.rb_live.toggled.connect(self.obs_type_changed)
|
|
127
133
|
self.rb_images.toggled.connect(self.obs_type_changed)
|
|
128
134
|
|
|
129
|
-
|
|
135
|
+
# button menu for media
|
|
136
|
+
|
|
137
|
+
add_media_menu_items = [
|
|
130
138
|
"media abs path|with absolute path",
|
|
131
139
|
"media rel path|with relative path",
|
|
132
140
|
{
|
|
@@ -136,23 +144,68 @@ class Observation(QDialog, Ui_Form):
|
|
|
136
144
|
]
|
|
137
145
|
},
|
|
138
146
|
]
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
|
|
148
|
+
self.media_menu = QMenu()
|
|
149
|
+
# Add actions to the menu
|
|
150
|
+
"""
|
|
151
|
+
self.action1 = QAction("with absolute path")
|
|
152
|
+
self.action2 = QAction("with relative path")
|
|
153
|
+
self.action3 = QAction("directory with absolute path")
|
|
154
|
+
self.action4 = QAction("directory with relative path")
|
|
155
|
+
|
|
156
|
+
self.menu.addAction(self.action1)
|
|
157
|
+
self.menu.addAction(self.action2)
|
|
158
|
+
self.menu.addAction(self.action3)
|
|
159
|
+
self.menu.addAction(self.action4)
|
|
160
|
+
|
|
161
|
+
# Connect actions to functions
|
|
162
|
+
self.action1.triggered.connect(lambda: self.add_media(mode="media abs path|with absolute path"))
|
|
163
|
+
self.action2.triggered.connect(lambda: self.add_media(mode="media rel path|with relative path"))
|
|
164
|
+
self.action3.triggered.connect(lambda: self.add_media(mode="dir abs path|with absolute path"))
|
|
165
|
+
self.action4.triggered.connect(lambda: self.add_media(mode="dir rel path|wih relative path"))
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
self.media_menu.triggered.connect(lambda x: self.add_media(mode=x.statusTip()))
|
|
169
|
+
self.add_button_menu(add_media_menu_items, self.media_menu)
|
|
170
|
+
self.pbAddVideo.setMenu(self.media_menu)
|
|
143
171
|
|
|
144
172
|
self.pbRemoveVideo.clicked.connect(self.remove_media)
|
|
145
173
|
|
|
146
|
-
#
|
|
174
|
+
# button menu for data file
|
|
147
175
|
data_menu_items = [
|
|
148
176
|
"data abs path|with absolute path",
|
|
149
177
|
"data rel path|with relative path",
|
|
150
178
|
]
|
|
151
179
|
|
|
152
|
-
menu_data = QMenu()
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
180
|
+
self.menu_data = QMenu()
|
|
181
|
+
|
|
182
|
+
# Add actions to the menu
|
|
183
|
+
"""
|
|
184
|
+
self.data_action1 = QAction("with absolute path")
|
|
185
|
+
self.data_action2 = QAction("with relative path")
|
|
186
|
+
self.menu_data.addAction(self.data_action1)
|
|
187
|
+
self.menu_data.addAction(self.data_action2)
|
|
188
|
+
|
|
189
|
+
# Connect actions to functions
|
|
190
|
+
self.data_action1.triggered.connect(lambda: self.add_data_file(mode="data abs path|with absolute path"))
|
|
191
|
+
self.data_action2.triggered.connect(lambda: self.add_data_file(mode="data rel path|with relative path"))
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
self.menu_data.triggered.connect(lambda x: self.add_data_file(mode=x.statusTip()))
|
|
195
|
+
self.add_button_menu(data_menu_items, self.menu_data)
|
|
196
|
+
self.pb_add_data_file.setMenu(self.menu_data)
|
|
197
|
+
|
|
198
|
+
# button menu for images
|
|
199
|
+
images_menu_items = [
|
|
200
|
+
"images abs path|with absolute path",
|
|
201
|
+
"images rel path|with relative path",
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
self.menu_images = QMenu()
|
|
205
|
+
|
|
206
|
+
self.menu_images.triggered.connect(lambda x: self.add_images_directory(mode=x.statusTip()))
|
|
207
|
+
self.add_button_menu(images_menu_items, self.menu_images)
|
|
208
|
+
self.pb_add_directory.setMenu(self.menu_images)
|
|
156
209
|
|
|
157
210
|
self.pb_remove_data_file.clicked.connect(self.remove_data_file)
|
|
158
211
|
self.pb_view_data_head.clicked.connect(self.view_data_file_head_tail)
|
|
@@ -163,6 +216,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
163
216
|
|
|
164
217
|
self.cbVisualizeSpectrogram.clicked.connect(self.extract_wav)
|
|
165
218
|
self.cb_visualize_waveform.clicked.connect(self.extract_wav)
|
|
219
|
+
|
|
166
220
|
self.cb_observation_time_interval.clicked.connect(self.limit_time_interval)
|
|
167
221
|
|
|
168
222
|
self.pbSave.clicked.connect(self.pbSave_clicked)
|
|
@@ -171,20 +225,23 @@ class Observation(QDialog, Ui_Form):
|
|
|
171
225
|
|
|
172
226
|
self.tw_data_files.cellDoubleClicked[int, int].connect(self.tw_data_files_cellDoubleClicked)
|
|
173
227
|
|
|
174
|
-
self.mediaDurations, self.mediaFPS, self.mediaHasVideo, self.mediaHasAudio = {}, {}, {}, {}
|
|
228
|
+
self.mediaDurations, self.mediaFPS, self.mediaHasVideo, self.mediaHasAudio, self.media_creation_time = {}, {}, {}, {}, {}
|
|
175
229
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
230
|
+
for w in (
|
|
231
|
+
self.cbVisualizeSpectrogram,
|
|
232
|
+
self.cb_visualize_waveform,
|
|
233
|
+
self.cb_observation_time_interval,
|
|
234
|
+
self.cb_media_creation_date_as_offset,
|
|
235
|
+
self.cbCloseCurrentBehaviorsBetweenVideo,
|
|
236
|
+
):
|
|
237
|
+
w.setEnabled(False)
|
|
179
238
|
|
|
180
|
-
|
|
181
|
-
self.cbCloseCurrentBehaviorsBetweenVideo.setChecked(False)
|
|
182
|
-
self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
|
|
239
|
+
self.cb_observation_time_interval.setEnabled(True)
|
|
183
240
|
|
|
184
241
|
self.cb_start_from_current_time.stateChanged.connect(self.cb_start_from_current_time_changed)
|
|
185
242
|
|
|
186
243
|
# images
|
|
187
|
-
self.pb_add_directory.clicked.connect(self.add_images_directory)
|
|
244
|
+
# self.pb_add_directory.clicked.connect(self.add_images_directory)
|
|
188
245
|
self.pb_remove_directory.clicked.connect(self.remove_images_directory)
|
|
189
246
|
|
|
190
247
|
self.tabWidget.setCurrentIndex(0)
|
|
@@ -192,6 +249,60 @@ class Observation(QDialog, Ui_Form):
|
|
|
192
249
|
# geometry
|
|
193
250
|
gui_utilities.restore_geometry(self, "new observation", (800, 650))
|
|
194
251
|
|
|
252
|
+
# def cb_date_offset_changed(self):
|
|
253
|
+
# """
|
|
254
|
+
# activate/desactivate time value
|
|
255
|
+
# """
|
|
256
|
+
# self.de_date_offset.setEnabled(self.cb_date_offset.isChecked())
|
|
257
|
+
|
|
258
|
+
def check_media_creation_date(self):
|
|
259
|
+
"""
|
|
260
|
+
check if all media files contain creation date time
|
|
261
|
+
search in metadata then in filename
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
creation_date_not_found: list = []
|
|
265
|
+
flag_filename_used = False
|
|
266
|
+
|
|
267
|
+
self.media_creation_time = {}
|
|
268
|
+
|
|
269
|
+
if self.cb_media_creation_date_as_offset.isChecked():
|
|
270
|
+
for row in range(self.twVideo1.rowCount()):
|
|
271
|
+
if self.twVideo1.item(row, 2).text(): # media file path
|
|
272
|
+
date_time_original = util.extract_video_creation_date(
|
|
273
|
+
project_functions.full_path(self.twVideo1.item(row, 2).text(), self.project_path)
|
|
274
|
+
)
|
|
275
|
+
if date_time_original is None:
|
|
276
|
+
date_time_file_name = util.extract_date_time_from_file_name(self.twVideo1.item(row, 2).text())
|
|
277
|
+
if date_time_file_name is None:
|
|
278
|
+
creation_date_not_found.append(self.twVideo1.item(row, 2).text())
|
|
279
|
+
else:
|
|
280
|
+
self.media_creation_time[self.twVideo1.item(row, 2).text()] = date_time_file_name
|
|
281
|
+
flag_filename_used = True
|
|
282
|
+
else:
|
|
283
|
+
self.media_creation_time[self.twVideo1.item(row, 2).text()] = date_time_original
|
|
284
|
+
|
|
285
|
+
if creation_date_not_found:
|
|
286
|
+
QMessageBox.warning(
|
|
287
|
+
self, cfg.programName, "The creation date time was not found for all media file(s).\nThe option was disabled."
|
|
288
|
+
)
|
|
289
|
+
self.cb_media_creation_date_as_offset.setChecked(False)
|
|
290
|
+
self.media_creation_time = {}
|
|
291
|
+
return 1
|
|
292
|
+
|
|
293
|
+
elif flag_filename_used:
|
|
294
|
+
QMessageBox.information(
|
|
295
|
+
self, cfg.programName, "The creation date time was not found in metadata. The media file name(s) was/were used"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return 0
|
|
299
|
+
|
|
300
|
+
def cb_time_offset_changed(self):
|
|
301
|
+
"""
|
|
302
|
+
activate/desactivate date value
|
|
303
|
+
"""
|
|
304
|
+
self.obs_time_offset.setEnabled(self.cb_time_offset.isChecked())
|
|
305
|
+
|
|
195
306
|
def use_media_file_name_as_obsid(self) -> None:
|
|
196
307
|
"""
|
|
197
308
|
set observation id with the media file name value (without path)
|
|
@@ -200,7 +311,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
200
311
|
QMessageBox.critical(self, cfg.programName, "A media file must be loaded in player #1")
|
|
201
312
|
return
|
|
202
313
|
|
|
203
|
-
first_media_file = ""
|
|
314
|
+
first_media_file: str = ""
|
|
204
315
|
for row in range(self.twVideo1.rowCount()):
|
|
205
316
|
if int(self.twVideo1.cellWidget(row, 0).currentText()) == 1:
|
|
206
317
|
first_media_file = self.twVideo1.item(row, 2).text()
|
|
@@ -235,11 +346,39 @@ class Observation(QDialog, Ui_Form):
|
|
|
235
346
|
# hide 'limit observation to time interval' for images
|
|
236
347
|
self.cb_observation_time_interval.setEnabled(not self.rb_images.isChecked())
|
|
237
348
|
|
|
238
|
-
def add_images_directory(self):
|
|
349
|
+
def add_images_directory(self, mode: str):
|
|
239
350
|
"""
|
|
240
351
|
add path to images directory
|
|
241
352
|
"""
|
|
242
|
-
|
|
353
|
+
|
|
354
|
+
if mode.split("|")[0] not in (
|
|
355
|
+
"images abs path",
|
|
356
|
+
"images rel path",
|
|
357
|
+
):
|
|
358
|
+
QMessageBox.critical(
|
|
359
|
+
self,
|
|
360
|
+
cfg.programName,
|
|
361
|
+
(f"Wrong mode to add a pictures directory {mode}"),
|
|
362
|
+
)
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
# check if project saved
|
|
366
|
+
if (" w/o" in mode or " rel " in mode) and (not self.project_file_name):
|
|
367
|
+
QMessageBox.critical(
|
|
368
|
+
self,
|
|
369
|
+
cfg.programName,
|
|
370
|
+
("It is not possible to add a pictures directory with a relative path if the project is not already saved"),
|
|
371
|
+
)
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
fd = QFileDialog()
|
|
375
|
+
fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
|
|
376
|
+
|
|
377
|
+
dir_path = fd.getExistingDirectory(None, "Select directory")
|
|
378
|
+
|
|
379
|
+
if not dir_path:
|
|
380
|
+
return
|
|
381
|
+
|
|
243
382
|
result = util.dir_images_number(dir_path)
|
|
244
383
|
if not result.get("number of images", 0):
|
|
245
384
|
response = dialog.MessageDialog(
|
|
@@ -250,7 +389,25 @@ class Observation(QDialog, Ui_Form):
|
|
|
250
389
|
if response == "Cancel":
|
|
251
390
|
return
|
|
252
391
|
|
|
253
|
-
|
|
392
|
+
# store directory for next usage
|
|
393
|
+
self.mem_dir = str(pl.Path(dir_path))
|
|
394
|
+
|
|
395
|
+
if " rel " in mode:
|
|
396
|
+
try:
|
|
397
|
+
pl.Path(dir_path).parent.relative_to(pl.Path(self.project_path).parent)
|
|
398
|
+
except ValueError:
|
|
399
|
+
QMessageBox.critical(
|
|
400
|
+
self,
|
|
401
|
+
cfg.programName,
|
|
402
|
+
f"The directory <b>{pl.Path(dir_path).parent}</b> is not contained in <b>{pl.Path(self.project_path).parent}</b>.",
|
|
403
|
+
)
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
if " rel " in mode:
|
|
407
|
+
# convert to relative path (relative to BORIS project file)
|
|
408
|
+
self.lw_images_directory.addItem(QListWidgetItem(str(pl.Path(dir_path).relative_to(pl.Path(self.project_path).parent))))
|
|
409
|
+
else:
|
|
410
|
+
self.lw_images_directory.addItem(QListWidgetItem(dir_path))
|
|
254
411
|
self.lb_images_info.setText(f"Number of images in {dir_path}: {result.get('number of images', 0)}")
|
|
255
412
|
|
|
256
413
|
def remove_images_directory(self):
|
|
@@ -290,10 +447,14 @@ class Observation(QDialog, Ui_Form):
|
|
|
290
447
|
"""
|
|
291
448
|
|
|
292
449
|
if self.cb_observation_time_interval.isChecked():
|
|
293
|
-
time_interval_dialog = dialog.Ask_time(
|
|
450
|
+
time_interval_dialog = dialog.Ask_time(0)
|
|
451
|
+
if self.time_format == cfg.S:
|
|
452
|
+
time_interval_dialog.time_widget.rb_seconds.setChecked(True)
|
|
453
|
+
if self.time_format == cfg.HHMMSS:
|
|
454
|
+
time_interval_dialog.time_widget.rb_time.setChecked(True)
|
|
294
455
|
time_interval_dialog.time_widget.set_time(0)
|
|
295
456
|
time_interval_dialog.setWindowTitle("Start observation at")
|
|
296
|
-
time_interval_dialog.label.setText("Start observation at")
|
|
457
|
+
time_interval_dialog.label.setText("<b>Start</b> observation at")
|
|
297
458
|
start_time, stop_time = 0, 0
|
|
298
459
|
if time_interval_dialog.exec_():
|
|
299
460
|
start_time = time_interval_dialog.time_widget.get_time()
|
|
@@ -302,7 +463,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
302
463
|
return
|
|
303
464
|
time_interval_dialog.time_widget.set_time(0)
|
|
304
465
|
time_interval_dialog.setWindowTitle("Stop observation at")
|
|
305
|
-
time_interval_dialog.label.setText("Stop observation at")
|
|
466
|
+
time_interval_dialog.label.setText("<b>Stop</b> observation at")
|
|
306
467
|
if time_interval_dialog.exec_():
|
|
307
468
|
stop_time = time_interval_dialog.time_widget.get_time()
|
|
308
469
|
else:
|
|
@@ -316,7 +477,10 @@ class Observation(QDialog, Ui_Form):
|
|
|
316
477
|
return
|
|
317
478
|
self.observation_time_interval = [start_time, stop_time]
|
|
318
479
|
self.cb_observation_time_interval.setText(
|
|
319
|
-
|
|
480
|
+
(
|
|
481
|
+
"Limit observation to a time interval: "
|
|
482
|
+
f"{util.smart_time_format(start_time, self.time_format)} - {util.smart_time_format(stop_time, self.time_format)}"
|
|
483
|
+
)
|
|
320
484
|
)
|
|
321
485
|
else:
|
|
322
486
|
self.observation_time_interval = [0, 0]
|
|
@@ -338,9 +502,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
338
502
|
|
|
339
503
|
if w.exec_():
|
|
340
504
|
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
|
-
):
|
|
505
|
+
for col_idx, cb in zip(self.tw_data_files.item(row, cfg.PLOT_DATA_COLUMNS_IDX).text().split(","), w.cbb):
|
|
344
506
|
if cb.currentText() != "None":
|
|
345
507
|
d[col_idx] = cb.currentText()
|
|
346
508
|
self.tw_data_files.item(row, cfg.PLOT_DATA_CONVERTERS_IDX).setText(str(d))
|
|
@@ -361,7 +523,6 @@ class Observation(QDialog, Ui_Form):
|
|
|
361
523
|
return
|
|
362
524
|
|
|
363
525
|
if self.tw_data_files.selectedIndexes() or self.tw_data_files.rowCount() == 1:
|
|
364
|
-
|
|
365
526
|
if self.tw_data_files.rowCount() == 1:
|
|
366
527
|
row_idx = 0
|
|
367
528
|
else:
|
|
@@ -381,9 +542,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
381
542
|
time_interval = int(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_TIMEINTERVAL_IDX).text())
|
|
382
543
|
time_offset = int(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_TIMEOFFSET_IDX).text())
|
|
383
544
|
|
|
384
|
-
substract_first_value = self.tw_data_files.cellWidget(
|
|
385
|
-
row_idx, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX
|
|
386
|
-
).currentText()
|
|
545
|
+
substract_first_value = self.tw_data_files.cellWidget(row_idx, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX).currentText()
|
|
387
546
|
|
|
388
547
|
plot_color = self.tw_data_files.cellWidget(row_idx, cfg.PLOT_DATA_PLOTCOLOR_IDX).currentText()
|
|
389
548
|
|
|
@@ -428,6 +587,13 @@ class Observation(QDialog, Ui_Form):
|
|
|
428
587
|
else:
|
|
429
588
|
QMessageBox.warning(self, cfg.programName, "Select a data file")
|
|
430
589
|
|
|
590
|
+
def not_editable_column_color(self):
|
|
591
|
+
"""
|
|
592
|
+
return a color for the not editable column
|
|
593
|
+
"""
|
|
594
|
+
window_color = QApplication.instance().palette().window().color()
|
|
595
|
+
return QColor(window_color.red() - 5, window_color.green() - 5, window_color.blue() - 5)
|
|
596
|
+
|
|
431
597
|
def add_data_file(self, mode: str):
|
|
432
598
|
"""
|
|
433
599
|
user select a data file to be plotted synchronously with media file
|
|
@@ -461,19 +627,14 @@ class Observation(QDialog, Ui_Form):
|
|
|
461
627
|
QMessageBox.warning(
|
|
462
628
|
self,
|
|
463
629
|
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
|
-
),
|
|
630
|
+
("It is not yet possible to plot more than 2 external data sourcesThis limitation will be removed in future"),
|
|
468
631
|
)
|
|
469
632
|
return
|
|
470
633
|
|
|
471
634
|
fd = QFileDialog()
|
|
472
635
|
fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
|
|
473
636
|
|
|
474
|
-
|
|
475
|
-
file_name = fn[0] if type(fn) is tuple else fn
|
|
476
|
-
|
|
637
|
+
file_name, _ = fd.getOpenFileName(self, "Add data file", "", "All files (*)")
|
|
477
638
|
if not file_name:
|
|
478
639
|
return
|
|
479
640
|
|
|
@@ -490,16 +651,14 @@ class Observation(QDialog, Ui_Form):
|
|
|
490
651
|
QMessageBox.critical(self, cfg.programName, "This file does not contain a constant number of columns")
|
|
491
652
|
return
|
|
492
653
|
|
|
493
|
-
header, footer = util.return_file_header_footer(
|
|
494
|
-
file_name, file_row_number=file_parameters["rows number"], row_number=5
|
|
495
|
-
)
|
|
654
|
+
header, footer = util.return_file_header_footer(file_name, file_row_number=file_parameters["rows number"], row_number=5)
|
|
496
655
|
|
|
497
656
|
if not header:
|
|
498
657
|
QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(file_name).name}")
|
|
499
658
|
return
|
|
500
659
|
|
|
501
660
|
w = dialog.View_data()
|
|
502
|
-
w.setWindowTitle(
|
|
661
|
+
w.setWindowTitle("View data")
|
|
503
662
|
w.lb.setText(f"View first and last rows of <b>{pl.Path(file_name).name}</b> file")
|
|
504
663
|
|
|
505
664
|
w.tw.setColumnCount(file_parameters["fields number"])
|
|
@@ -518,9 +677,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
518
677
|
|
|
519
678
|
# stats
|
|
520
679
|
try:
|
|
521
|
-
df = pd.read_csv(
|
|
522
|
-
file_name, sep=file_parameters["separator"], header=None if not file_parameters["has header"] else [0]
|
|
523
|
-
)
|
|
680
|
+
df = pd.read_csv(file_name, sep=file_parameters["separator"], header=None if not file_parameters["has header"] else [0])
|
|
524
681
|
# set columns names to based 1 index
|
|
525
682
|
if not file_parameters["has header"]:
|
|
526
683
|
df.columns = range(1, len(df.columns) + 1)
|
|
@@ -556,7 +713,6 @@ class Observation(QDialog, Ui_Form):
|
|
|
556
713
|
self.tw_data_files.setRowCount(self.tw_data_files.rowCount() + 1)
|
|
557
714
|
|
|
558
715
|
if " rel " in mode:
|
|
559
|
-
|
|
560
716
|
try:
|
|
561
717
|
file_path = str(pl.Path(file_name).relative_to(pl.Path(self.project_path).parent))
|
|
562
718
|
except ValueError:
|
|
@@ -585,15 +741,14 @@ class Observation(QDialog, Ui_Form):
|
|
|
585
741
|
item = QTableWidgetItem(value)
|
|
586
742
|
if col_idx == cfg.PLOT_DATA_CONVERTERS_IDX:
|
|
587
743
|
item.setFlags(Qt.ItemIsEnabled)
|
|
588
|
-
item.setBackground(QColor(230, 230, 230))
|
|
744
|
+
# item.setBackground(QColor(230, 230, 230))
|
|
745
|
+
item.setBackground(self.not_editable_column_color())
|
|
589
746
|
self.tw_data_files.setItem(self.tw_data_files.rowCount() - 1, col_idx, item)
|
|
590
747
|
|
|
591
748
|
# substract first value
|
|
592
749
|
combobox = QComboBox()
|
|
593
750
|
combobox.addItems(["True", "False"])
|
|
594
|
-
self.tw_data_files.setCellWidget(
|
|
595
|
-
self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX, combobox
|
|
596
|
-
)
|
|
751
|
+
self.tw_data_files.setCellWidget(self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX, combobox)
|
|
597
752
|
|
|
598
753
|
# plot line color
|
|
599
754
|
combobox = QComboBox()
|
|
@@ -622,18 +777,16 @@ class Observation(QDialog, Ui_Form):
|
|
|
622
777
|
if "error" in file_parameters:
|
|
623
778
|
QMessageBox.critical(self, cfg.programName, f"Error on file {data_file_path}: {file_parameters['error']}")
|
|
624
779
|
return
|
|
625
|
-
header, footer = util.return_file_header_footer(
|
|
626
|
-
data_file_path, file_row_number=file_parameters["rows number"], row_number=5
|
|
627
|
-
)
|
|
780
|
+
header, footer = util.return_file_header_footer(data_file_path, file_row_number=file_parameters["rows number"], row_number=5)
|
|
628
781
|
|
|
629
782
|
if not header:
|
|
630
783
|
QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(data_file_path).name}")
|
|
631
784
|
return
|
|
632
785
|
|
|
633
786
|
w = dialog.View_data()
|
|
634
|
-
w.setWindowTitle(
|
|
787
|
+
w.setWindowTitle("View data")
|
|
635
788
|
w.lb.setText(f"View first and last rows of <b>{pl.Path(data_file_path).name}</b> file")
|
|
636
|
-
w.pbOK.setText(
|
|
789
|
+
w.pbOK.setText(cfg.CLOSE)
|
|
637
790
|
w.label.setText("Index of columns to plot")
|
|
638
791
|
w.le.setEnabled(False)
|
|
639
792
|
w.le.setText(columns_to_plot)
|
|
@@ -676,73 +829,139 @@ class Observation(QDialog, Ui_Form):
|
|
|
676
829
|
extract wav of all media files loaded in player #1
|
|
677
830
|
"""
|
|
678
831
|
|
|
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
|
|
832
|
+
if not self.cbVisualizeSpectrogram.isChecked() and not self.cb_visualize_waveform.isChecked():
|
|
833
|
+
return
|
|
686
834
|
|
|
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:
|
|
835
|
+
flag_wav_produced = False
|
|
836
|
+
# check if player 1 is selected
|
|
837
|
+
flag_player1 = False
|
|
838
|
+
for row in range(self.twVideo1.rowCount()):
|
|
839
|
+
if self.twVideo1.cellWidget(row, 0).currentText() == "1":
|
|
840
|
+
flag_player1 = True
|
|
697
841
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
842
|
+
if not flag_player1:
|
|
843
|
+
QMessageBox.critical(self, cfg.programName, "The player #1 is not selected")
|
|
844
|
+
self.cbVisualizeSpectrogram.setChecked(False)
|
|
845
|
+
self.cb_visualize_waveform.setChecked(False)
|
|
846
|
+
return
|
|
703
847
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
848
|
+
if True:
|
|
849
|
+
w = dialog.Info_widget()
|
|
850
|
+
w.resize(350, 100)
|
|
851
|
+
# w.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
852
|
+
w.setWindowTitle("BORIS")
|
|
853
|
+
w.label.setText("Extracting WAV from media files...")
|
|
708
854
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
855
|
+
for row in range(self.twVideo1.rowCount()):
|
|
856
|
+
# check if player 1
|
|
857
|
+
if self.twVideo1.cellWidget(row, 0).currentText() != "1":
|
|
858
|
+
continue
|
|
859
|
+
|
|
860
|
+
media_file_path = project_functions.full_path(self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text(), self.project_path)
|
|
861
|
+
if self.twVideo1.item(row, cfg.HAS_AUDIO_IDX).text() == "False":
|
|
862
|
+
QMessageBox.critical(self, cfg.programName, f"The media file {media_file_path} does not seem to have audio")
|
|
863
|
+
flag_wav_produced = False
|
|
864
|
+
break
|
|
865
|
+
|
|
866
|
+
if os.path.isfile(media_file_path):
|
|
867
|
+
w.show()
|
|
868
|
+
QApplication.processEvents()
|
|
869
|
+
|
|
870
|
+
if util.extract_wav(self.ffmpeg_bin, media_file_path, self.tmp_dir) == "":
|
|
713
871
|
QMessageBox.critical(
|
|
714
|
-
self,
|
|
872
|
+
self,
|
|
873
|
+
cfg.programName,
|
|
874
|
+
f"Error during extracting WAV of the media file {media_file_path}",
|
|
715
875
|
)
|
|
716
876
|
flag_wav_produced = False
|
|
717
877
|
break
|
|
718
878
|
|
|
719
|
-
|
|
720
|
-
w.show()
|
|
721
|
-
QApplication.processEvents()
|
|
879
|
+
w.hide()
|
|
722
880
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
break
|
|
881
|
+
flag_wav_produced = True
|
|
882
|
+
else:
|
|
883
|
+
QMessageBox.warning(self, cfg.programName, f"<b>{media_file_path}</b> file not found")
|
|
884
|
+
|
|
885
|
+
if not flag_wav_produced:
|
|
886
|
+
self.cbVisualizeSpectrogram.setChecked(False)
|
|
887
|
+
self.cb_visualize_waveform.setChecked(False)
|
|
731
888
|
|
|
732
|
-
|
|
889
|
+
def check_creation_date(self) -> int:
|
|
890
|
+
"""
|
|
891
|
+
check if media file exists
|
|
892
|
+
check if Creation Date tag is present in metadata of media file
|
|
733
893
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
QMessageBox.warning(self, cfg.programName, f"<b>{media_file_path}</b> file not found")
|
|
894
|
+
Returns:
|
|
895
|
+
int: 0 if OK else error code: 1 -> media file date not used, 2 -> media file not found
|
|
737
896
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
897
|
+
"""
|
|
898
|
+
|
|
899
|
+
# check if media files exist
|
|
900
|
+
|
|
901
|
+
media_not_found_list: list = []
|
|
902
|
+
for row in range(self.twVideo1.rowCount()):
|
|
903
|
+
if not pl.Path(self.twVideo1.item(row, 2).text()).is_file():
|
|
904
|
+
media_not_found_list.append(self.twVideo1.item(row, 2).text())
|
|
905
|
+
|
|
906
|
+
"""
|
|
907
|
+
if media_list:
|
|
908
|
+
dlg = dialog.Results_dialog()
|
|
909
|
+
dlg.setWindowTitle("BORIS")
|
|
910
|
+
dlg.pbOK.setText("OK")
|
|
911
|
+
dlg.pbCancel.setVisible(False)
|
|
912
|
+
dlg.ptText.clear()
|
|
913
|
+
dlg.ptText.appendHtml(
|
|
914
|
+
(
|
|
915
|
+
"Some media file(s) were not found:<br>"
|
|
916
|
+
f"{'<br>'.join(media_list)}<br><br>"
|
|
917
|
+
"You cannot select the <b>Use the media creation date/time option</b>."
|
|
918
|
+
)
|
|
919
|
+
)
|
|
920
|
+
dlg.ptText.moveCursor(QTextCursor.Start)
|
|
921
|
+
ret = dlg.exec_()
|
|
922
|
+
"""
|
|
923
|
+
|
|
924
|
+
"""
|
|
925
|
+
not_tagged_media_list: list = []
|
|
926
|
+
for row in range(self.twVideo1.rowCount()):
|
|
927
|
+
if self.twVideo1.item(row, 2).text() not in media_not_found_list:
|
|
928
|
+
media_info = util.accurate_media_analysis(self.ffmpeg_bin, self.twVideo1.item(row, 2).text())
|
|
929
|
+
if cfg.MEDIA_CREATION_TIME not in media_info or media_info[cfg.MEDIA_CREATION_TIME] == cfg.NA:
|
|
930
|
+
not_tagged_media_list.append(self.twVideo1.item(row, 2).text())
|
|
931
|
+
else:
|
|
932
|
+
creation_time_epoch = int(dt.datetime.strptime(media_info[cfg.MEDIA_CREATION_TIME], "%Y-%m-%d %H:%M:%S").timestamp())
|
|
933
|
+
self.media_creation_time[self.twVideo1.item(row, 2).text()] = creation_time_epoch
|
|
934
|
+
|
|
935
|
+
if not_tagged_media_list:
|
|
936
|
+
dlg = dialog.Results_dialog()
|
|
937
|
+
dlg.setWindowTitle("BORIS")
|
|
938
|
+
dlg.pbOK.setText("Yes")
|
|
939
|
+
dlg.pbCancel.setVisible(True)
|
|
940
|
+
dlg.pbCancel.setText("No")
|
|
941
|
+
|
|
942
|
+
dlg.ptText.clear()
|
|
943
|
+
dlg.ptText.appendHtml(
|
|
944
|
+
(
|
|
945
|
+
"Some media file does not contain the <b>Creation date/time</b> metadata tag:<br>"
|
|
946
|
+
f"{'<br>'.join(not_tagged_media_list)}<br><br>"
|
|
947
|
+
"Use the media file date/time instead?"
|
|
948
|
+
)
|
|
949
|
+
)
|
|
950
|
+
dlg.ptText.moveCursor(QTextCursor.Start)
|
|
951
|
+
ret = dlg.exec_()
|
|
952
|
+
|
|
953
|
+
if ret == 1: # use file creation time
|
|
954
|
+
for media in not_tagged_media_list:
|
|
955
|
+
self.media_creation_time[media] = pl.Path(media).stat().st_ctime
|
|
956
|
+
return 0 # OK use media file creation date/time
|
|
742
957
|
else:
|
|
743
|
-
self.
|
|
744
|
-
self.
|
|
745
|
-
|
|
958
|
+
self.cb_media_creation_date_as_offset.setChecked(False)
|
|
959
|
+
self.media_creation_time = {}
|
|
960
|
+
return 1
|
|
961
|
+
else:
|
|
962
|
+
return 0 # OK all media have a 'creation time' tag
|
|
963
|
+
"""
|
|
964
|
+
return 0
|
|
746
965
|
|
|
747
966
|
def closeEvent(self, event):
|
|
748
967
|
"""
|
|
@@ -761,11 +980,13 @@ class Observation(QDialog, Ui_Form):
|
|
|
761
980
|
self.text = None
|
|
762
981
|
self.reject()
|
|
763
982
|
|
|
764
|
-
def check_parameters(self):
|
|
983
|
+
def check_parameters(self) -> bool:
|
|
765
984
|
"""
|
|
766
985
|
check observation parameters
|
|
767
986
|
|
|
768
|
-
|
|
987
|
+
Returns:
|
|
988
|
+
bool: True if everything is OK else False
|
|
989
|
+
|
|
769
990
|
"""
|
|
770
991
|
|
|
771
992
|
def is_numeric(s):
|
|
@@ -786,21 +1007,40 @@ class Observation(QDialog, Ui_Form):
|
|
|
786
1007
|
|
|
787
1008
|
# check if observation id not empty
|
|
788
1009
|
if not self.leObservationId.text():
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1010
|
+
QMessageBox.critical(
|
|
1011
|
+
self,
|
|
1012
|
+
cfg.programName,
|
|
1013
|
+
"The <b>observation id</b> is mandatory and must be unique.",
|
|
1014
|
+
)
|
|
793
1015
|
return False
|
|
794
1016
|
|
|
795
1017
|
# check if observation_type
|
|
796
1018
|
if not any((self.rb_media_files.isChecked(), self.rb_live.isChecked(), self.rb_images.isChecked())):
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1019
|
+
QMessageBox.critical(
|
|
1020
|
+
self,
|
|
1021
|
+
cfg.programName,
|
|
1022
|
+
"Choose an observation type.",
|
|
1023
|
+
)
|
|
801
1024
|
return False
|
|
802
1025
|
|
|
1026
|
+
# check if offset is correct
|
|
1027
|
+
if self.cb_time_offset.isChecked():
|
|
1028
|
+
if self.obs_time_offset.get_time() is None:
|
|
1029
|
+
QMessageBox.critical(
|
|
1030
|
+
self,
|
|
1031
|
+
cfg.programName,
|
|
1032
|
+
"Check the time offset value.",
|
|
1033
|
+
)
|
|
1034
|
+
return False
|
|
1035
|
+
|
|
803
1036
|
if self.rb_media_files.isChecked(): # observation based on media file(s)
|
|
1037
|
+
# check if media file exists
|
|
1038
|
+
media_file_not_found: list = []
|
|
1039
|
+
for row in range(self.twVideo1.rowCount()):
|
|
1040
|
+
# check if media file exists
|
|
1041
|
+
if not pl.Path(self.twVideo1.item(row, 2).text()).is_file():
|
|
1042
|
+
media_file_not_found.append(self.twVideo1.item(row, 2).text())
|
|
1043
|
+
|
|
804
1044
|
# check player number
|
|
805
1045
|
players_list: list = []
|
|
806
1046
|
players: dict = {} # for storing duration
|
|
@@ -813,18 +1053,20 @@ class Observation(QDialog, Ui_Form):
|
|
|
813
1053
|
|
|
814
1054
|
# check if player #1 is used
|
|
815
1055
|
if not players_list or min(players_list) > 1:
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1056
|
+
QMessageBox.critical(
|
|
1057
|
+
self,
|
|
1058
|
+
cfg.programName,
|
|
1059
|
+
"A media file must be loaded in player #1",
|
|
1060
|
+
)
|
|
820
1061
|
return False
|
|
821
1062
|
|
|
822
1063
|
# check if players are used in crescent order
|
|
823
1064
|
if set(list(range(min(players_list), max(players_list) + 1))) != set(players_list):
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1065
|
+
QMessageBox.critical(
|
|
1066
|
+
self,
|
|
1067
|
+
cfg.programName,
|
|
1068
|
+
"Some player are not used. Please reorganize your media files",
|
|
1069
|
+
)
|
|
828
1070
|
return False
|
|
829
1071
|
|
|
830
1072
|
# check if more media in player #1 and media in other players
|
|
@@ -856,7 +1098,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
856
1098
|
return False
|
|
857
1099
|
|
|
858
1100
|
# check that the longuest media is in player #1
|
|
859
|
-
durations = []
|
|
1101
|
+
durations: list = []
|
|
860
1102
|
for i in sorted(list(players.keys())):
|
|
861
1103
|
durations.append(sum(players[i]))
|
|
862
1104
|
if [x for x in durations[1:] if x > durations[0]]:
|
|
@@ -878,6 +1120,20 @@ class Observation(QDialog, Ui_Form):
|
|
|
878
1120
|
)
|
|
879
1121
|
return False
|
|
880
1122
|
|
|
1123
|
+
# check if offset set and only player #1 is used
|
|
1124
|
+
if len(set(players_list)) == 1:
|
|
1125
|
+
for row in range(self.twVideo1.rowCount()):
|
|
1126
|
+
if float(self.twVideo1.item(row, 1).text()):
|
|
1127
|
+
QMessageBox.critical(
|
|
1128
|
+
self,
|
|
1129
|
+
cfg.programName,
|
|
1130
|
+
(
|
|
1131
|
+
"It is not possible to use offset value(s) with only one player,<br>"
|
|
1132
|
+
"The offset values are use to synchronise various players."
|
|
1133
|
+
),
|
|
1134
|
+
)
|
|
1135
|
+
return False
|
|
1136
|
+
|
|
881
1137
|
# check offset for external data files
|
|
882
1138
|
for row in range(self.tw_data_files.rowCount()):
|
|
883
1139
|
if not is_numeric(self.tw_data_files.item(row, cfg.PLOT_DATA_TIMEOFFSET_IDX).text()):
|
|
@@ -893,6 +1149,18 @@ class Observation(QDialog, Ui_Form):
|
|
|
893
1149
|
)
|
|
894
1150
|
return False
|
|
895
1151
|
|
|
1152
|
+
# check media creation time tag in metadata
|
|
1153
|
+
# Disable because the check will be made at the observation start
|
|
1154
|
+
"""
|
|
1155
|
+
if self.cb_media_creation_date_as_offset.isChecked():
|
|
1156
|
+
if self.check_creation_date():
|
|
1157
|
+
return False
|
|
1158
|
+
"""
|
|
1159
|
+
|
|
1160
|
+
# check media creation date time (if option enabled)
|
|
1161
|
+
if self.check_media_creation_date():
|
|
1162
|
+
return False
|
|
1163
|
+
|
|
896
1164
|
if self.rb_images.isChecked(): # observation based on images directory
|
|
897
1165
|
if not self.lw_images_directory.count():
|
|
898
1166
|
QMessageBox.critical(self, cfg.programName, "You have to select at least one images directory")
|
|
@@ -901,9 +1169,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
901
1169
|
# check if indep variables are correct type
|
|
902
1170
|
for row in range(self.twIndepVariables.rowCount()):
|
|
903
1171
|
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
|
-
):
|
|
1172
|
+
if self.twIndepVariables.item(row, 2).text() and not is_numeric(self.twIndepVariables.item(row, 2).text()):
|
|
907
1173
|
QMessageBox.critical(
|
|
908
1174
|
self,
|
|
909
1175
|
cfg.programName,
|
|
@@ -925,11 +1191,10 @@ class Observation(QDialog, Ui_Form):
|
|
|
925
1191
|
)
|
|
926
1192
|
return False
|
|
927
1193
|
|
|
1194
|
+
# check if numeric indep variable values are numeric
|
|
928
1195
|
for row in range(self.twIndepVariables.rowCount()):
|
|
929
1196
|
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
|
-
):
|
|
1197
|
+
if self.twIndepVariables.item(row, 2).text() and not is_numeric(self.twIndepVariables.item(row, 2).text()):
|
|
933
1198
|
QMessageBox.critical(
|
|
934
1199
|
self,
|
|
935
1200
|
cfg.programName,
|
|
@@ -941,7 +1206,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
941
1206
|
|
|
942
1207
|
def pbLaunch_clicked(self):
|
|
943
1208
|
"""
|
|
944
|
-
Close
|
|
1209
|
+
Close dialog and start the observation
|
|
945
1210
|
"""
|
|
946
1211
|
|
|
947
1212
|
if self.check_parameters():
|
|
@@ -976,24 +1241,54 @@ class Observation(QDialog, Ui_Form):
|
|
|
976
1241
|
str: error message or empty string
|
|
977
1242
|
"""
|
|
978
1243
|
|
|
1244
|
+
logging.debug(f"check_media function for {file_path}")
|
|
1245
|
+
|
|
979
1246
|
media_info = util.accurate_media_analysis(self.ffmpeg_bin, file_path)
|
|
1247
|
+
|
|
1248
|
+
logging.debug(f"{media_info=}")
|
|
1249
|
+
|
|
980
1250
|
if "error" in media_info:
|
|
981
|
-
return
|
|
1251
|
+
return (True, media_info["error"])
|
|
1252
|
+
|
|
1253
|
+
if media_info["format_long_name"] == "Tele-typewriter":
|
|
1254
|
+
return (True, "Text file")
|
|
1255
|
+
|
|
1256
|
+
if media_info["duration"] > 0:
|
|
1257
|
+
if " rel " in mode:
|
|
1258
|
+
# convert to relative path (relative to BORIS project file)
|
|
1259
|
+
file_path = str(pl.Path(file_path).relative_to(pl.Path(self.project_path).parent))
|
|
1260
|
+
|
|
1261
|
+
self.mediaDurations[file_path] = float(media_info["duration"])
|
|
1262
|
+
elif media_info["has_video"] is False and media_info["audio_duration"]:
|
|
1263
|
+
self.mediaDurations[file_path] = float(media_info["audio_duration"])
|
|
982
1264
|
else:
|
|
983
|
-
|
|
1265
|
+
return (True, "Media duration not available")
|
|
984
1266
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1267
|
+
self.mediaFPS[file_path] = float(media_info["fps"])
|
|
1268
|
+
self.mediaHasVideo[file_path] = media_info["has_video"]
|
|
1269
|
+
self.mediaHasAudio[file_path] = media_info["has_audio"]
|
|
1270
|
+
|
|
1271
|
+
logging.debug(f"{file_path=}")
|
|
1272
|
+
|
|
1273
|
+
self.add_media_to_listview(file_path)
|
|
1274
|
+
return (False, "")
|
|
1275
|
+
|
|
1276
|
+
def update_media_options(self):
|
|
1277
|
+
"""
|
|
1278
|
+
update the media options
|
|
1279
|
+
"""
|
|
1280
|
+
for w in (
|
|
1281
|
+
self.cbVisualizeSpectrogram,
|
|
1282
|
+
self.cb_visualize_waveform,
|
|
1283
|
+
self.cb_observation_time_interval,
|
|
1284
|
+
self.cb_media_creation_date_as_offset,
|
|
1285
|
+
):
|
|
1286
|
+
w.setEnabled(self.twVideo1.rowCount() > 0)
|
|
1287
|
+
|
|
1288
|
+
# enable stop ongoing state events if n. media > 1
|
|
1289
|
+
self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(self.twVideo1.rowCount() > 0)
|
|
1290
|
+
|
|
1291
|
+
# self.creation_date_as_offset()
|
|
997
1292
|
|
|
998
1293
|
def add_media(self, mode: str):
|
|
999
1294
|
"""
|
|
@@ -1021,9 +1316,7 @@ class Observation(QDialog, Ui_Form):
|
|
|
1021
1316
|
QMessageBox.critical(
|
|
1022
1317
|
self,
|
|
1023
1318
|
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
|
-
),
|
|
1319
|
+
("It is not possible to add a media file without path or with a relative path if the project is not already saved"),
|
|
1027
1320
|
)
|
|
1028
1321
|
return
|
|
1029
1322
|
|
|
@@ -1034,9 +1327,9 @@ class Observation(QDialog, Ui_Form):
|
|
|
1034
1327
|
fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
|
|
1035
1328
|
|
|
1036
1329
|
if "media " in mode:
|
|
1330
|
+
file_paths, _ = fd.getOpenFileNames(self, "Add media file(s)", "", "All files (*)")
|
|
1037
1331
|
|
|
1038
|
-
|
|
1039
|
-
file_paths = fn[0] if type(fn) is tuple else fn
|
|
1332
|
+
logging.debug(f"{file_paths=}")
|
|
1040
1333
|
|
|
1041
1334
|
if file_paths:
|
|
1042
1335
|
# store directory for next usage
|
|
@@ -1058,39 +1351,40 @@ class Observation(QDialog, Ui_Form):
|
|
|
1058
1351
|
if error:
|
|
1059
1352
|
QMessageBox.critical(self, cfg.programName, f"<b>{file_path}</b>. {msg}")
|
|
1060
1353
|
|
|
1061
|
-
if "dir " in mode:
|
|
1062
|
-
|
|
1354
|
+
if "dir " in mode: # add media from dir
|
|
1063
1355
|
dir_name = fd.getExistingDirectory(self, "Select directory")
|
|
1064
1356
|
if dir_name:
|
|
1065
1357
|
response = ""
|
|
1066
|
-
for file_path in
|
|
1067
|
-
|
|
1358
|
+
for file_path in sorted(pl.Path(dir_name).glob("*")):
|
|
1359
|
+
if not file_path.is_file():
|
|
1360
|
+
continue
|
|
1361
|
+
(error, msg) = self.check_media(str(file_path), mode)
|
|
1068
1362
|
if error:
|
|
1069
1363
|
if response != "Skip all non media files":
|
|
1070
1364
|
response = dialog.MessageDialog(
|
|
1071
1365
|
cfg.programName,
|
|
1072
1366
|
f"<b>{file_path}</b> {msg}",
|
|
1073
|
-
["Continue", "Skip all non media files",
|
|
1367
|
+
["Continue", "Skip all non media files", cfg.CANCEL],
|
|
1074
1368
|
)
|
|
1075
|
-
if response ==
|
|
1369
|
+
if response == cfg.CANCEL:
|
|
1076
1370
|
break
|
|
1371
|
+
# ask to use directory name / path as observation id
|
|
1372
|
+
if response != cfg.CANCEL:
|
|
1373
|
+
selected_obs_id = dialog.MessageDialog(
|
|
1374
|
+
cfg.programName,
|
|
1375
|
+
"Select the observation id",
|
|
1376
|
+
[dir_name, str(pl.Path(dir_name).name), cfg.CANCEL],
|
|
1377
|
+
)
|
|
1378
|
+
if selected_obs_id != cfg.CANCEL:
|
|
1379
|
+
self.leObservationId.setText(selected_obs_id)
|
|
1077
1380
|
|
|
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)
|
|
1381
|
+
self.update_media_options()
|
|
1088
1382
|
|
|
1089
1383
|
def add_media_to_listview(self, file_name):
|
|
1090
1384
|
"""
|
|
1091
1385
|
add media file path to list widget
|
|
1092
1386
|
"""
|
|
1093
|
-
|
|
1387
|
+
# add a row
|
|
1094
1388
|
self.twVideo1.setRowCount(self.twVideo1.rowCount() + 1)
|
|
1095
1389
|
|
|
1096
1390
|
for col_idx, s in enumerate(
|
|
@@ -1131,33 +1425,22 @@ class Observation(QDialog, Ui_Form):
|
|
|
1131
1425
|
remove all selected media files from list widget
|
|
1132
1426
|
"""
|
|
1133
1427
|
|
|
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:
|
|
1428
|
+
if not self.twVideo1.selectedIndexes():
|
|
1163
1429
|
QMessageBox.warning(self, cfg.programName, "No media file selected")
|
|
1430
|
+
return
|
|
1431
|
+
|
|
1432
|
+
rows_to_delete = set([x.row() for x in self.twVideo1.selectedIndexes()])
|
|
1433
|
+
for row in sorted(rows_to_delete, reverse=True):
|
|
1434
|
+
media_path = self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text()
|
|
1435
|
+
self.twVideo1.removeRow(row)
|
|
1436
|
+
if media_path not in [self.twVideo1.item(idx, cfg.MEDIA_FILE_PATH_IDX).text() for idx in range(self.twVideo1.rowCount())]:
|
|
1437
|
+
try:
|
|
1438
|
+
del self.mediaDurations[media_path]
|
|
1439
|
+
except NameError:
|
|
1440
|
+
pass
|
|
1441
|
+
try:
|
|
1442
|
+
del self.mediaFPS[media_path]
|
|
1443
|
+
except NameError:
|
|
1444
|
+
pass
|
|
1445
|
+
|
|
1446
|
+
self.update_media_options()
|