boris-behav-obs 9.4.1__tar.gz → 9.5__tar.gz
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_behav_obs-9.4.1/boris_behav_obs.egg-info → boris_behav_obs-9.5}/PKG-INFO +4 -1
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/analysis_plugins/_latency.py +1 -1
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/config.py +10 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/connections.py +2 -1
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/core.py +2 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/core_ui.py +6 -2
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/external_processes.py +98 -73
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/import_observations.py +28 -19
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/observation_operations.py +15 -1
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/plot_events.py +1 -1
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/plot_events_rt.py +1 -1
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/plot_spectrogram_rt.py +62 -13
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/plot_waveform_rt.py +1 -1
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/plugins.py +136 -25
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/preferences.py +182 -108
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/preferences_ui.py +216 -32
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/project_functions.py +1 -3
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/utilities.py +21 -14
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/version.py +2 -2
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5/boris_behav_obs.egg-info}/PKG-INFO +4 -1
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris_behav_obs.egg-info/requires.txt +4 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/pyproject.toml +3 -1
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/LICENSE.TXT +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/MANIFEST.in +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/README.TXT +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/README.md +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/__init__.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/__main__.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/about.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/add_modifier.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/add_modifier_ui.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/advanced_event_filtering.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/analysis_plugins/__init__.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/analysis_plugins/number_of_occurences.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/analysis_plugins/number_of_occurences_by_independent_variable.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/analysis_plugins/time_budget.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/behav_coding_map_creator.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/behavior_binary_table.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/behaviors_coding_map.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/boris_cli.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/cmd_arguments.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/coding_pad.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/config_file.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/converters.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/converters_ui.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/cooccurence.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/core_qrc.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/db_functions.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/dev.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/dialog.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/duration_widget.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/edit_event.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/edit_event_ui.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/event_operations.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/events_cursor.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/events_snapshots.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/exclusion_matrix.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/export_events.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/export_observation.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/geometric_measurement.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/gui_utilities.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/image_overlay.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/irr.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/latency.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/measurement_widget.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/media_file.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/menu_options.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/modifier_coding_map_creator.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/modifiers_coding_map.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/mpv-1.0.3.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/mpv.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/mpv2.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/observation.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/observation_ui.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/observations_list.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/otx_parser.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/param_panel.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/param_panel_ui.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/player_dock_widget.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/plot_data_module.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/portion/__init__.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/portion/const.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/portion/dict.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/portion/func.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/portion/interval.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/portion/io.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/project.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/project_import_export.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/project_ui.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/qrc_boris.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/qrc_boris5.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/select_modifiers.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/select_observations.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/select_subj_behav.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/state_events.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/subjects_pad.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/synthetic_time_budget.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/time_budget_functions.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/time_budget_widget.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/transitions.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/video_equalizer.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/video_equalizer_ui.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/video_operations.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/view_df.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/view_df_ui.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris/write_event.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris_behav_obs.egg-info/SOURCES.txt +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris_behav_obs.egg-info/dependency_links.txt +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris_behav_obs.egg-info/entry_points.txt +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/boris_behav_obs.egg-info/top_level.txt +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/setup.cfg +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/tests/test_db_functions.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/tests/test_export_observation.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/tests/test_irr.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/tests/test_observation_gui.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/tests/test_otx_parser.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/tests/test_preferences_gui.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/tests/test_project_functions.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/tests/test_time_budget.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/tests/test_utilities.py +0 -0
- {boris_behav_obs-9.4.1 → boris_behav_obs-9.5}/tests/test_utilities2.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boris-behav-obs
|
|
3
|
-
Version: 9.
|
|
3
|
+
Version: 9.5
|
|
4
4
|
Summary: BORIS - Behavioral Observation Research Interactive Software
|
|
5
5
|
Author-email: Olivier Friard <olivier.friard@unito.it>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
@@ -26,10 +26,13 @@ Requires-Dist: tablib[cli,html,ods,pandas,xls,xlsx]>=3
|
|
|
26
26
|
Requires-Dist: pyreadr
|
|
27
27
|
Requires-Dist: pyside6==6.9
|
|
28
28
|
Requires-Dist: hachoir>=3.3.0
|
|
29
|
+
Requires-Dist: scipy>=1.15.3
|
|
29
30
|
Provides-Extra: dev
|
|
30
31
|
Requires-Dist: ruff; extra == "dev"
|
|
31
32
|
Requires-Dist: pytest; extra == "dev"
|
|
32
33
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
34
|
+
Provides-Extra: r
|
|
35
|
+
Requires-Dist: rpy2>=3.6.1; extra == "r"
|
|
33
36
|
Dynamic: license-file
|
|
34
37
|
|
|
35
38
|
BORIS (Behavioral Observation Research Interactive Software)
|
|
@@ -531,6 +531,16 @@ NO_COLOR_CODING_PAD = "#777777"
|
|
|
531
531
|
SPECTROGRAM_COLOR_MAPS = ["viridis", "inferno", "plasma", "magma", "gray", "YlOrRd"]
|
|
532
532
|
SPECTROGRAM_DEFAULT_COLOR_MAP = "viridis"
|
|
533
533
|
SPECTROGRAM_DEFAULT_TIME_INTERVAL = 10
|
|
534
|
+
SPECTROGRAM_WINDOW_TYPE = "SPECTROGRAM_WINDOW_TYPE"
|
|
535
|
+
SPECTROGRAM_DEFAULT_WINDOW_TYPE = "hanning"
|
|
536
|
+
SPECTROGRAM_NFFT = "SPECTROGRAM_NFFT"
|
|
537
|
+
SPECTROGRAM_DEFAULT_NFFT = "1024"
|
|
538
|
+
SPECTROGRAM_NOVERLAP = "SPECTROGRAM_NOVERLAP"
|
|
539
|
+
SPECTROGRAM_DEFAULT_NOVERLAP = 900
|
|
540
|
+
SPECTROGRAM_VMIN = "SPECTROGRAM_VMIN"
|
|
541
|
+
SPECTROGRAM_DEFAULT_VMIN = -100
|
|
542
|
+
SPECTROGRAM_VMAX = "SPECTROGRAM_VMAX"
|
|
543
|
+
SPECTROGRAM_DEFAULT_VMAX = -20
|
|
534
544
|
|
|
535
545
|
# see matplotlib.colors.cnames.keys()
|
|
536
546
|
# https://xkcd.com/color/rgb/
|
|
@@ -193,10 +193,11 @@ def connections(self):
|
|
|
193
193
|
self.actionAdd_image_overlay_on_video.triggered.connect(lambda: image_overlay.add_image_overlay(self))
|
|
194
194
|
self.actionRemove_image_overlay.triggered.connect(lambda: image_overlay.remove_image_overlay(self))
|
|
195
195
|
|
|
196
|
+
self.actionMedia_file_information_2.triggered.connect(lambda: media_file.get_info(self))
|
|
196
197
|
self.actionRecode_resize_video.triggered.connect(lambda: external_processes.ffmpeg_process(self, "reencode_resize"))
|
|
197
198
|
self.actionRotate_video.triggered.connect(lambda: external_processes.ffmpeg_process(self, "rotate"))
|
|
198
199
|
self.actionMerge_media_files.triggered.connect(lambda: external_processes.ffmpeg_process(self, "merge"))
|
|
199
|
-
self.
|
|
200
|
+
self.actionCreate_video_spectrogram.triggered.connect(lambda: external_processes.ffmpeg_process(self, "video_spectrogram"))
|
|
200
201
|
|
|
201
202
|
self.actionCreate_transitions_flow_diagram.triggered.connect(transitions.transitions_dot_script)
|
|
202
203
|
self.actionCreate_transitions_flow_diagram_2.triggered.connect(transitions.transitions_flow_diagram)
|
|
@@ -1034,6 +1034,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
|
|
1034
1034
|
self.spectro.interval = self.spectrogram_time_interval
|
|
1035
1035
|
self.spectro.cursor_color = cfg.REALTIME_PLOT_CURSOR_COLOR
|
|
1036
1036
|
|
|
1037
|
+
self.spectro.config_param = self.config_param
|
|
1038
|
+
|
|
1037
1039
|
# color palette
|
|
1038
1040
|
try:
|
|
1039
1041
|
self.spectro.spectro_color_map = matplotlib.pyplot.get_cmap(self.spectrogram_color_map)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
################################################################################
|
|
4
4
|
## Form generated from reading UI file 'core.ui'
|
|
5
5
|
##
|
|
6
|
-
## Created by: Qt User Interface Compiler version 6.
|
|
6
|
+
## Created by: Qt User Interface Compiler version 6.9.0
|
|
7
7
|
##
|
|
8
8
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
|
9
9
|
################################################################################
|
|
@@ -378,6 +378,8 @@ class Ui_MainWindow(object):
|
|
|
378
378
|
self.actionAdd_frame_indexes.setObjectName(u"actionAdd_frame_indexes")
|
|
379
379
|
self.action_load_plugins = QAction(MainWindow)
|
|
380
380
|
self.action_load_plugins.setObjectName(u"action_load_plugins")
|
|
381
|
+
self.actionCreate_video_spectrogram = QAction(MainWindow)
|
|
382
|
+
self.actionCreate_video_spectrogram.setObjectName(u"actionCreate_video_spectrogram")
|
|
381
383
|
self.centralwidget = QWidget(MainWindow)
|
|
382
384
|
self.centralwidget.setObjectName(u"centralwidget")
|
|
383
385
|
self.horizontalLayout_2 = QHBoxLayout(self.centralwidget)
|
|
@@ -484,7 +486,7 @@ class Ui_MainWindow(object):
|
|
|
484
486
|
MainWindow.setCentralWidget(self.centralwidget)
|
|
485
487
|
self.menubar = QMenuBar(MainWindow)
|
|
486
488
|
self.menubar.setObjectName(u"menubar")
|
|
487
|
-
self.menubar.setGeometry(QRect(0, 0, 1509,
|
|
489
|
+
self.menubar.setGeometry(QRect(0, 0, 1509, 20))
|
|
488
490
|
self.menuHelp = QMenu(self.menubar)
|
|
489
491
|
self.menuHelp.setObjectName(u"menuHelp")
|
|
490
492
|
self.menuFile = QMenu(self.menubar)
|
|
@@ -775,6 +777,7 @@ class Ui_MainWindow(object):
|
|
|
775
777
|
self.menuMedia_file.addAction(self.actionRecode_resize_video)
|
|
776
778
|
self.menuMedia_file.addAction(self.actionRotate_video)
|
|
777
779
|
self.menuMedia_file.addAction(self.actionMerge_media_files)
|
|
780
|
+
self.menuMedia_file.addAction(self.actionCreate_video_spectrogram)
|
|
778
781
|
self.toolBar.addAction(self.action_obs_list)
|
|
779
782
|
self.toolBar.addAction(self.actionPlay)
|
|
780
783
|
self.toolBar.addAction(self.actionReset)
|
|
@@ -1029,6 +1032,7 @@ class Ui_MainWindow(object):
|
|
|
1029
1032
|
self.actionConfigure_tvevents_columns.setText(QCoreApplication.translate("MainWindow", u"Configure columns", None))
|
|
1030
1033
|
self.actionAdd_frame_indexes.setText(QCoreApplication.translate("MainWindow", u"Add frame indexes", None))
|
|
1031
1034
|
self.action_load_plugins.setText(QCoreApplication.translate("MainWindow", u"Load plugins", None))
|
|
1035
|
+
self.actionCreate_video_spectrogram.setText(QCoreApplication.translate("MainWindow", u"Create video spectrogram", None))
|
|
1032
1036
|
self.lbLogoBoris.setText("")
|
|
1033
1037
|
self.lbLogoUnito.setText("")
|
|
1034
1038
|
self.lb_player_status.setText(QCoreApplication.translate("MainWindow", u"lb_player_status", None))
|
|
@@ -22,7 +22,7 @@ This file is part of BORIS.
|
|
|
22
22
|
|
|
23
23
|
import os
|
|
24
24
|
import tempfile
|
|
25
|
-
|
|
25
|
+
from pathlib import Path
|
|
26
26
|
import logging
|
|
27
27
|
|
|
28
28
|
from PySide6.QtWidgets import QFileDialog, QMessageBox, QInputDialog
|
|
@@ -41,9 +41,9 @@ def ffmpeg_process(self, action: str):
|
|
|
41
41
|
launch ffmpeg process with QProcess
|
|
42
42
|
|
|
43
43
|
Args:
|
|
44
|
-
action (str): "reencode_resize, rotate, merge
|
|
44
|
+
action (str): "reencode_resize, rotate, merge, video_spectrogram
|
|
45
45
|
"""
|
|
46
|
-
if action not in ("reencode_resize", "rotate", "merge"):
|
|
46
|
+
if action not in ("reencode_resize", "rotate", "merge", "video_spectrogram"):
|
|
47
47
|
return
|
|
48
48
|
|
|
49
49
|
def readStdOutput(idx):
|
|
@@ -61,7 +61,7 @@ def ffmpeg_process(self, action: str):
|
|
|
61
61
|
# self.processes_widget.lwi.clear()
|
|
62
62
|
std_out = self.processes[idx - 1][0].readAllStandardOutput().data().decode("utf-8")
|
|
63
63
|
if std_out:
|
|
64
|
-
self.processes_widget.lwi.addItems((f"{
|
|
64
|
+
self.processes_widget.lwi.addItems((f"{Path(self.processes[idx - 1][1][2]).name}: {std_out}",))
|
|
65
65
|
|
|
66
66
|
"""
|
|
67
67
|
std_err = self.processes[idx - 1][0].readAllStandardError().data().decode("utf-8")
|
|
@@ -153,7 +153,7 @@ def ffmpeg_process(self, action: str):
|
|
|
153
153
|
file_list_lst = []
|
|
154
154
|
for file_name in file_names:
|
|
155
155
|
file_list_lst.append(f"file '{file_name}'")
|
|
156
|
-
file_extensions.append(
|
|
156
|
+
file_extensions.append(Path(file_name).suffix)
|
|
157
157
|
if len(set(file_extensions)) > 1:
|
|
158
158
|
QMessageBox.critical(self, cfg.programName, "All media files must have the same format")
|
|
159
159
|
return
|
|
@@ -162,13 +162,13 @@ def ffmpeg_process(self, action: str):
|
|
|
162
162
|
output_file_name, _ = QFileDialog().getSaveFileName(self, "Output file name", "", "*")
|
|
163
163
|
if output_file_name == "":
|
|
164
164
|
return
|
|
165
|
-
if
|
|
165
|
+
if Path(output_file_name).suffix != file_extensions[0]:
|
|
166
166
|
QMessageBox.warning(
|
|
167
167
|
self,
|
|
168
168
|
cfg.programName,
|
|
169
169
|
(
|
|
170
170
|
"The extension of output file must be the same than the extension of input files "
|
|
171
|
-
f"(<b>{file_extensions[0]}</b>).<br>You selected a {
|
|
171
|
+
f"(<b>{file_extensions[0]}</b>).<br>You selected a {Path(output_file_name).suffix} file."
|
|
172
172
|
),
|
|
173
173
|
)
|
|
174
174
|
else:
|
|
@@ -215,93 +215,118 @@ def ffmpeg_process(self, action: str):
|
|
|
215
215
|
self.processes_widget.resize(700, 300)
|
|
216
216
|
|
|
217
217
|
self.processes_widget.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
218
|
+
match action:
|
|
219
|
+
case "reencode_resize":
|
|
220
|
+
self.processes_widget.setWindowTitle("Re-encoding and resizing with FFmpeg")
|
|
221
|
+
case "rotate":
|
|
222
|
+
self.processes_widget.setWindowTitle("Rotating the video with FFmpeg")
|
|
223
|
+
case "merge":
|
|
224
|
+
self.processes_widget.setWindowTitle("Merging media files")
|
|
225
|
+
case "video_spectrogram":
|
|
226
|
+
self.processes_widget.setWindowTitle("Creating a video spectrogram")
|
|
224
227
|
|
|
225
228
|
self.processes_widget.label.setText("This operation can be long. Be patient...\nIn the meanwhile you can continue to use BORIS\n\n")
|
|
226
229
|
self.processes_widget.number_of_files = len(file_names)
|
|
227
230
|
self.processes_widget.show()
|
|
228
231
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
232
|
+
match action:
|
|
233
|
+
case "merge":
|
|
234
|
+
# ffmpeg -f concat -safe 0 -i join_video.txt -c copy output.mp4
|
|
235
|
+
args = ["-hide_banner", "-y", "-f", "concat", "-safe", "0", "-i", file_list, "-c", "copy", output_file_name]
|
|
236
|
+
self.processes.append([QProcess(self), [self.ffmpeg_bin, args, output_file_name]])
|
|
237
|
+
self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
|
|
238
|
+
self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
|
|
239
|
+
self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
|
|
240
|
+
self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
|
|
237
241
|
|
|
238
|
-
|
|
242
|
+
self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
|
|
239
243
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
case "video_spectrogram":
|
|
245
|
+
# ffmpeg -i video.mp4 -filter_complex showspectrum=mode=combined:color=intensity:slide=1:scale=cbrt -y -acodec copy output.mp4
|
|
246
|
+
for file_name in sorted(file_names, reverse=True):
|
|
247
|
+
output_file_name = str(Path(file_name).with_suffix(f".spectrogram{Path(file_name).suffix}"))
|
|
243
248
|
args = [
|
|
244
249
|
"-hide_banner",
|
|
245
250
|
"-y",
|
|
246
251
|
"-i",
|
|
247
|
-
|
|
248
|
-
"-
|
|
249
|
-
|
|
250
|
-
"-
|
|
251
|
-
|
|
252
|
-
|
|
252
|
+
file_name,
|
|
253
|
+
"-filter_complex",
|
|
254
|
+
"showspectrum=mode=combined:color=intensity:slide=1:scale=cbrt",
|
|
255
|
+
"-acodec",
|
|
256
|
+
"copy",
|
|
257
|
+
output_file_name,
|
|
253
258
|
]
|
|
259
|
+
self.processes.append([QProcess(self), [self.ffmpeg_bin, args, output_file_name]])
|
|
260
|
+
self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
|
|
261
|
+
self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
|
|
262
|
+
self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
|
|
263
|
+
self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
|
|
254
264
|
|
|
255
|
-
|
|
256
|
-
# check bitrate
|
|
257
|
-
r = util.accurate_media_analysis(self.ffmpeg_bin, file_name)
|
|
258
|
-
if "error" not in r and r["bitrate"] is not None:
|
|
259
|
-
current_bitrate = r["bitrate"]
|
|
260
|
-
else:
|
|
261
|
-
current_bitrate = 10_000_000
|
|
262
|
-
|
|
263
|
-
if rotation_idx in (1, 2):
|
|
264
|
-
args = [
|
|
265
|
-
"-hide_banner",
|
|
266
|
-
"-y",
|
|
267
|
-
"-i",
|
|
268
|
-
f"{file_name}",
|
|
269
|
-
"-vf",
|
|
270
|
-
f"transpose={rotation_idx}",
|
|
271
|
-
"-codec:a",
|
|
272
|
-
"copy",
|
|
273
|
-
"-b:v",
|
|
274
|
-
f"{current_bitrate}",
|
|
275
|
-
f"{file_name}.rotated{['', '90', '-90'][rotation_idx]}.avi",
|
|
276
|
-
]
|
|
265
|
+
self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
|
|
277
266
|
|
|
278
|
-
|
|
267
|
+
case "reencode_resize" | "rotate":
|
|
268
|
+
for file_name in sorted(file_names, reverse=True):
|
|
269
|
+
if action == "reencode_resize":
|
|
279
270
|
args = [
|
|
280
271
|
"-hide_banner",
|
|
281
272
|
"-y",
|
|
282
273
|
"-i",
|
|
283
274
|
f"{file_name}",
|
|
284
275
|
"-vf",
|
|
285
|
-
"
|
|
286
|
-
"-codec:a",
|
|
287
|
-
"copy",
|
|
276
|
+
f"scale={horiz_resol}:-1",
|
|
288
277
|
"-b:v",
|
|
289
|
-
f"{
|
|
290
|
-
f"{file_name}.
|
|
278
|
+
f"{video_quality * 1024 * 1024}",
|
|
279
|
+
f"{file_name}.re-encoded.{horiz_resol}px.{video_quality}Mb.avi",
|
|
291
280
|
]
|
|
292
281
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
282
|
+
if action == "rotate":
|
|
283
|
+
# check bitrate
|
|
284
|
+
r = util.accurate_media_analysis(self.ffmpeg_bin, file_name)
|
|
285
|
+
if "error" not in r and r["bitrate"] is not None:
|
|
286
|
+
current_bitrate = r["bitrate"]
|
|
287
|
+
else:
|
|
288
|
+
current_bitrate = 10_000_000
|
|
289
|
+
|
|
290
|
+
if rotation_idx in (1, 2):
|
|
291
|
+
args = [
|
|
292
|
+
"-hide_banner",
|
|
293
|
+
"-y",
|
|
294
|
+
"-i",
|
|
295
|
+
f"{file_name}",
|
|
296
|
+
"-vf",
|
|
297
|
+
f"transpose={rotation_idx}",
|
|
298
|
+
"-codec:a",
|
|
299
|
+
"copy",
|
|
300
|
+
"-b:v",
|
|
301
|
+
f"{current_bitrate}",
|
|
302
|
+
f"{file_name}.rotated{['', '90', '-90'][rotation_idx]}.avi",
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
if rotation_idx == 3: # 180
|
|
306
|
+
args = [
|
|
307
|
+
"-hide_banner",
|
|
308
|
+
"-y",
|
|
309
|
+
"-i",
|
|
310
|
+
f"{file_name}",
|
|
311
|
+
"-vf",
|
|
312
|
+
"transpose=2,transpose=2",
|
|
313
|
+
"-codec:a",
|
|
314
|
+
"copy",
|
|
315
|
+
"-b:v",
|
|
316
|
+
f"{current_bitrate}",
|
|
317
|
+
f"{file_name}.rotated180.avi",
|
|
318
|
+
]
|
|
319
|
+
|
|
320
|
+
logging.debug("Launch process")
|
|
321
|
+
logging.debug(f"{self.ffmpeg_bin} {' '.join(args)}")
|
|
322
|
+
|
|
323
|
+
self.processes.append([QProcess(self), [self.ffmpeg_bin, args, file_name]])
|
|
324
|
+
|
|
325
|
+
## FFmpeg output the work in progress on stderr
|
|
326
|
+
self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
|
|
327
|
+
self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
|
|
328
|
+
# self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
|
|
329
|
+
|
|
330
|
+
self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
|
|
299
331
|
|
|
300
|
-
|
|
301
|
-
self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
|
|
302
|
-
self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
|
|
303
|
-
# self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
|
|
304
|
-
|
|
305
|
-
self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
|
|
306
|
-
|
|
307
|
-
self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
|
|
332
|
+
self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
|
|
@@ -19,10 +19,11 @@ Copyright 2012-2025 Olivier Friard
|
|
|
19
19
|
MA 02110-1301, USA.
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
import json
|
|
23
22
|
import datetime
|
|
24
|
-
|
|
23
|
+
import gzip
|
|
24
|
+
import json
|
|
25
25
|
import pandas as pd
|
|
26
|
+
from pathlib import Path
|
|
26
27
|
|
|
27
28
|
from PySide6.QtWidgets import (
|
|
28
29
|
QMessageBox,
|
|
@@ -49,8 +50,14 @@ def load_observations_from_boris_project(self, project_file_path: str):
|
|
|
49
50
|
)
|
|
50
51
|
return
|
|
51
52
|
|
|
53
|
+
if project_file_path.endswith(".boris.gz"):
|
|
54
|
+
file_in = gzip.open(project_file_path, mode="rt", encoding="utf-8")
|
|
55
|
+
else:
|
|
56
|
+
file_in = open(project_file_path, "r")
|
|
57
|
+
file_content = file_in.read()
|
|
58
|
+
|
|
52
59
|
try:
|
|
53
|
-
fromProject = json.loads(
|
|
60
|
+
fromProject = json.loads(file_content)
|
|
54
61
|
except Exception:
|
|
55
62
|
QMessageBox.critical(self, cfg.programName, "This project file seems corrupted")
|
|
56
63
|
return
|
|
@@ -84,7 +91,7 @@ def load_observations_from_boris_project(self, project_file_path: str):
|
|
|
84
91
|
if new_behav_set:
|
|
85
92
|
diag_result = dialog.MessageDialog(
|
|
86
93
|
cfg.programName,
|
|
87
|
-
(f"Some coded behaviors in <b>{obs_id}</b> are
|
|
94
|
+
(f"Some coded behaviors in <b>{obs_id}</b> are not defined in the ethogram:<br><b>{', '.join(new_behav_set)}</b>"),
|
|
88
95
|
["Interrupt import", "Skip observation", "Import observation"],
|
|
89
96
|
)
|
|
90
97
|
if diag_result == "Interrupt import":
|
|
@@ -103,7 +110,7 @@ def load_observations_from_boris_project(self, project_file_path: str):
|
|
|
103
110
|
if new_subject_set and new_subject_set != {""}:
|
|
104
111
|
diag_result = dialog.MessageDialog(
|
|
105
112
|
cfg.programName,
|
|
106
|
-
(f"Some coded subjects in <b>{obs_id}</b> are not defined in the project:<br
|
|
113
|
+
(f"Some coded subjects in <b>{obs_id}</b> are not defined in the project:<br><b>{', '.join(new_subject_set)}</b>"),
|
|
107
114
|
["Interrupt import", "Skip observation", "Import observation"],
|
|
108
115
|
)
|
|
109
116
|
|
|
@@ -116,7 +123,7 @@ def load_observations_from_boris_project(self, project_file_path: str):
|
|
|
116
123
|
if obs_id in self.pj[cfg.OBSERVATIONS].keys():
|
|
117
124
|
diag_result = dialog.MessageDialog(
|
|
118
125
|
cfg.programName,
|
|
119
|
-
(f"The observation <b>{obs_id}</b>
|
|
126
|
+
(f"The observation <b>{obs_id}</b>already exists in the current project.<br>"),
|
|
120
127
|
["Interrupt import", "Skip observation", "Rename observation"],
|
|
121
128
|
)
|
|
122
129
|
if diag_result == "Interrupt import":
|
|
@@ -141,18 +148,11 @@ def load_observations_from_spreadsheet(self, project_file_path: str):
|
|
|
141
148
|
import observations from a spreadsheet file
|
|
142
149
|
"""
|
|
143
150
|
|
|
144
|
-
if Path(project_file_path).suffix.
|
|
151
|
+
if Path(project_file_path).suffix.lower() == ".xlsx":
|
|
145
152
|
engine = "openpyxl"
|
|
146
|
-
elif Path(project_file_path).suffix.
|
|
153
|
+
elif Path(project_file_path).suffix.lower() == ".ods":
|
|
147
154
|
engine = "odf"
|
|
148
155
|
else:
|
|
149
|
-
QMessageBox.warning(
|
|
150
|
-
None,
|
|
151
|
-
cfg.programName,
|
|
152
|
-
("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
|
|
153
|
-
QMessageBox.Ok | QMessageBox.Default,
|
|
154
|
-
QMessageBox.NoButton,
|
|
155
|
-
)
|
|
156
156
|
return
|
|
157
157
|
|
|
158
158
|
try:
|
|
@@ -167,7 +167,7 @@ def load_observations_from_spreadsheet(self, project_file_path: str):
|
|
|
167
167
|
)
|
|
168
168
|
return
|
|
169
169
|
|
|
170
|
-
expected_labels: list =
|
|
170
|
+
expected_labels: list = ("time", "subject", "code", "modifier", "comment")
|
|
171
171
|
|
|
172
172
|
df.columns = df.columns.str.upper()
|
|
173
173
|
|
|
@@ -210,16 +210,16 @@ def import_observations(self):
|
|
|
210
210
|
"""
|
|
211
211
|
|
|
212
212
|
file_name, _ = QFileDialog().getOpenFileName(
|
|
213
|
-
None, "Choose a file", "", "BORIS project files (*.boris);;Spreadsheet files (*.ods *.xlsx *);;All files (*)"
|
|
213
|
+
None, "Choose a file", "", "BORIS project files (*.boris *.boris.gz);;Spreadsheet files (*.ods *.xlsx *);;All files (*)"
|
|
214
214
|
)
|
|
215
215
|
|
|
216
216
|
if not file_name:
|
|
217
217
|
return
|
|
218
218
|
|
|
219
|
-
if
|
|
219
|
+
if file_name.endswith(".boris") or file_name.endswith(".boris.gz"):
|
|
220
220
|
load_observations_from_boris_project(self, file_name)
|
|
221
221
|
|
|
222
|
-
|
|
222
|
+
elif Path(file_name).suffix.lower() in (".ods", ".xlsx"):
|
|
223
223
|
if not self.observationId:
|
|
224
224
|
QMessageBox.warning(
|
|
225
225
|
None,
|
|
@@ -231,3 +231,12 @@ def import_observations(self):
|
|
|
231
231
|
return
|
|
232
232
|
|
|
233
233
|
load_observations_from_spreadsheet(self, file_name)
|
|
234
|
+
|
|
235
|
+
else:
|
|
236
|
+
QMessageBox.warning(
|
|
237
|
+
None,
|
|
238
|
+
cfg.programName,
|
|
239
|
+
("The type of file was not recognized. Must be a BORIS project or a Microsoft-Excel XLSX format or OpenDocument ODS"),
|
|
240
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
241
|
+
QMessageBox.NoButton,
|
|
242
|
+
)
|
|
@@ -19,7 +19,7 @@ Copyright 2012-2025 Olivier Friard
|
|
|
19
19
|
MA 02110-1301, USA.
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
from math import log2
|
|
22
|
+
from math import log2, floor
|
|
23
23
|
import os
|
|
24
24
|
import logging
|
|
25
25
|
import time
|
|
@@ -2426,6 +2426,17 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
|
|
|
2426
2426
|
str: name of media file containing the event
|
|
2427
2427
|
"""
|
|
2428
2428
|
|
|
2429
|
+
cumul_media_durations: list = [dec(0)]
|
|
2430
|
+
for media_file in observation[cfg.FILE][cfg.PLAYER1]:
|
|
2431
|
+
try:
|
|
2432
|
+
media_duration = observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]
|
|
2433
|
+
# cut off media duration to 3 decimal places as that is how fine the player is
|
|
2434
|
+
media_duration = floor(media_duration * 10**3) / dec(10**3)
|
|
2435
|
+
cumul_media_durations.append(floor((cumul_media_durations[-1] + media_duration) * 10**3) / dec(10**3))
|
|
2436
|
+
except KeyError:
|
|
2437
|
+
return None
|
|
2438
|
+
|
|
2439
|
+
"""
|
|
2429
2440
|
cumul_media_durations: list = [dec(0)]
|
|
2430
2441
|
for media_file in observation[cfg.FILE][cfg.PLAYER1]:
|
|
2431
2442
|
try:
|
|
@@ -2433,9 +2444,12 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
|
|
|
2433
2444
|
cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
|
|
2434
2445
|
except KeyError:
|
|
2435
2446
|
return None
|
|
2447
|
+
"""
|
|
2436
2448
|
|
|
2437
2449
|
cumul_media_durations.remove(dec(0))
|
|
2438
2450
|
|
|
2451
|
+
logging.debug(f"{cumul_media_durations=}")
|
|
2452
|
+
|
|
2439
2453
|
# test if timestamp is at end of last media
|
|
2440
2454
|
if timestamp == cumul_media_durations[-1]:
|
|
2441
2455
|
player_idx = len(observation[cfg.FILE][cfg.PLAYER1]) - 1
|