boris-behav-obs 9.7.8__tar.gz → 9.8.1__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.7.8 → boris_behav_obs-9.8.1}/PKG-INFO +2 -2
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/README.md +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/__init__.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/__main__.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/about.py +4 -3
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/add_modifier.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/advanced_event_filtering.py +1 -1
- boris_behav_obs-9.8.1/boris/analysis_plugins/export_to_feral.py +336 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/irr_weighted_cohen_kappa.py +2 -2
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/behav_coding_map_creator.py +8 -8
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/behavior_binary_table.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/behaviors_coding_map.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/boris_cli.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/cmd_arguments.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/coding_pad.py +5 -4
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/config.py +15 -3
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/config_file.py +18 -19
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/connections.py +12 -13
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/converters.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/converters_ui.py +2 -3
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/cooccurence.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/core.py +187 -181
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/core_qrc.py +1830 -1967
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/core_ui.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/db_functions.py +5 -14
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/dialog.py +24 -24
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/edit_event.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/event_operations.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/events_cursor.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/events_snapshots.py +133 -78
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/exclusion_matrix.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/export_events.py +49 -43
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/export_observation.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/external_processes.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/geometric_measurement.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/gui_utilities.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/image_overlay.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/import_observations.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/ipc_mpv.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/irr.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/latency.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/measurement_widget.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/media_file.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/menu_options.py +14 -12
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/modifier_coding_map_creator.py +7 -7
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/modifiers_coding_map.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/observation.py +13 -14
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/observation_operations.py +49 -41
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/observations_list.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/otx_parser.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/param_panel.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/player_dock_widget.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/plot_data_module.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/plot_events.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/plot_events_rt.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/plot_spectrogram_rt.py +44 -72
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/plot_waveform_rt.py +5 -2
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/plugins.py +61 -24
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/preferences.py +35 -4
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/preferences_ui.py +50 -18
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/project.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/project_functions.py +20 -23
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/project_import_export.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/select_modifiers.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/select_observations.py +22 -23
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/select_subj_behav.py +4 -4
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/state_events.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/subjects_pad.py +3 -3
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/synthetic_time_budget.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/time_budget_functions.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/time_budget_widget.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/transitions.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/utilities.py +10 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/version.py +3 -3
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/video_equalizer.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/video_operations.py +4 -3
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/view_df.py +28 -4
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/write_event.py +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris_behav_obs.egg-info/PKG-INFO +2 -2
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris_behav_obs.egg-info/SOURCES.txt +1 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/pyproject.toml +1 -1
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/LICENSE.TXT +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/MANIFEST.in +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/README.TXT +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/add_modifier_ui.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/__init__.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/_latency.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/irr_cohen_kappa.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/list_of_dataframe_columns.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/number_of_occurences.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/number_of_occurences_by_independent_variable.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/time_budget.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/dev.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/duration_widget.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/edit_event_ui.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/mpv.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/mpv2.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/observation_ui.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/param_panel_ui.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/portion/__init__.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/portion/const.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/portion/dict.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/portion/func.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/portion/interval.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/portion/io.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/project_ui.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/qrc_boris.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/qrc_boris5.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/video_equalizer_ui.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/view_df_ui.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris_behav_obs.egg-info/dependency_links.txt +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris_behav_obs.egg-info/entry_points.txt +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris_behav_obs.egg-info/requires.txt +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris_behav_obs.egg-info/top_level.txt +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/setup.cfg +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_db_functions.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_export_observation.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_irr.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_observation_gui.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_otx_parser.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_preferences_gui.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_project_functions.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_time_budget.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_utilities.py +0 -0
- {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/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.8.1
|
|
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
|
|
@@ -51,7 +51,7 @@ It provides also some analysis tools like time budget and some plotting function
|
|
|
51
51
|
<!-- The BO-RIS paper has more than [ citations](https://www.boris.unito.it/citations) in peer-reviewed scientific publications. -->
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
The BORIS paper has more than
|
|
54
|
+
The BORIS paper has more than 2491 citations in peer-reviewed scientific publications.
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
|
|
@@ -13,7 +13,7 @@ It provides also some analysis tools like time budget and some plotting function
|
|
|
13
13
|
<!-- The BO-RIS paper has more than [ citations](https://www.boris.unito.it/citations) in peer-reviewed scientific publications. -->
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
The BORIS paper has more than
|
|
16
|
+
The BORIS paper has more than 2491 citations in peer-reviewed scientific publications.
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This program is free software; you can redistribute it and/or modify
|
|
7
7
|
it under the terms of the GNU General Public License as published by
|
|
@@ -80,7 +80,8 @@ def actionAbout_activated(self):
|
|
|
80
80
|
programs_versions.extend(["\nGraphViz", gv_result if "graphviz" in gv_result else "not installed", "https://www.graphviz.org/"])
|
|
81
81
|
|
|
82
82
|
about_dialog: QMessageBox = QMessageBox()
|
|
83
|
-
about_dialog.setIconPixmap(QPixmap(":/boris_unito"))
|
|
83
|
+
# about_dialog.setIconPixmap(QPixmap(":/boris_unito"))
|
|
84
|
+
about_dialog.setIconPixmap(QPixmap(":/dbios_unito"))
|
|
84
85
|
|
|
85
86
|
about_dialog.setWindowTitle(f"About {cfg.programName}")
|
|
86
87
|
about_dialog.setStandardButtons(QMessageBox.Ok)
|
|
@@ -90,7 +91,7 @@ def actionAbout_activated(self):
|
|
|
90
91
|
about_dialog.setInformativeText(
|
|
91
92
|
(
|
|
92
93
|
f"<b>{cfg.programName}</b> v. {version.__version__} - {version.__version_date__}"
|
|
93
|
-
"<p>Copyright © 2012-
|
|
94
|
+
"<p>Copyright © 2012-2026 Olivier Friard - Marco Gamba<br>"
|
|
94
95
|
"Department of Life Sciences and Systems Biology<br>"
|
|
95
96
|
"University of Torino - Italy<br>"
|
|
96
97
|
"<br>"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This program is free software; you can redistribute it and/or modify
|
|
7
7
|
it under the terms of the GNU General Public License as published by
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS plugin
|
|
3
|
+
|
|
4
|
+
Export observations to FERAL (getferal.ai)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from PySide6.QtCore import Qt
|
|
12
|
+
from PySide6.QtWidgets import (
|
|
13
|
+
QDialog,
|
|
14
|
+
QFileDialog,
|
|
15
|
+
QHBoxLayout,
|
|
16
|
+
QLabel,
|
|
17
|
+
QListWidget,
|
|
18
|
+
QListWidgetItem,
|
|
19
|
+
QPushButton,
|
|
20
|
+
QVBoxLayout,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__version__ = "0.3.2"
|
|
24
|
+
__version_date__ = "2025-12-19"
|
|
25
|
+
__plugin_name__ = "Export observations to FERAL"
|
|
26
|
+
__author__ = "Jacopo Razzauti - The Rockefeller University; Olivier Friard - University of Torino - Italy"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------
|
|
30
|
+
# Dialog: choose behaviors
|
|
31
|
+
# ---------------------------
|
|
32
|
+
class BehaviorSelectDialog(QDialog):
|
|
33
|
+
"""Select which BORIS behavior codes should become FERAL classes.
|
|
34
|
+
|
|
35
|
+
Class 0 is reserved for "other". Any behavior not selected is mapped to 0.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, behavior_codes, parent=None):
|
|
39
|
+
super().__init__(parent)
|
|
40
|
+
|
|
41
|
+
self.setWindowTitle("Select behaviors to export (0 is 'other')")
|
|
42
|
+
self.setModal(True)
|
|
43
|
+
|
|
44
|
+
main_layout = QVBoxLayout(self)
|
|
45
|
+
|
|
46
|
+
info = QLabel("Select behaviors to export.\nClass 0 is reserved for 'other'.\nUnselected behaviors are mapped to 0.")
|
|
47
|
+
info.setWordWrap(True)
|
|
48
|
+
main_layout.addWidget(info)
|
|
49
|
+
|
|
50
|
+
self.list_behaviors = QListWidget()
|
|
51
|
+
self.list_behaviors.setSelectionMode(QListWidget.ExtendedSelection)
|
|
52
|
+
|
|
53
|
+
for code in sorted(behavior_codes):
|
|
54
|
+
item = QListWidgetItem(code)
|
|
55
|
+
item.setSelected(True) # default: select all
|
|
56
|
+
self.list_behaviors.addItem(item)
|
|
57
|
+
|
|
58
|
+
main_layout.addWidget(self.list_behaviors)
|
|
59
|
+
|
|
60
|
+
buttons_layout = QHBoxLayout()
|
|
61
|
+
btn_all = QPushButton("Select all")
|
|
62
|
+
btn_none = QPushButton("Select none")
|
|
63
|
+
btn_ok = QPushButton("OK")
|
|
64
|
+
btn_cancel = QPushButton("Cancel")
|
|
65
|
+
|
|
66
|
+
btn_all.clicked.connect(self._select_all)
|
|
67
|
+
btn_none.clicked.connect(self._select_none)
|
|
68
|
+
btn_ok.clicked.connect(self.accept)
|
|
69
|
+
btn_cancel.clicked.connect(self.reject)
|
|
70
|
+
|
|
71
|
+
buttons_layout.addWidget(btn_all)
|
|
72
|
+
buttons_layout.addWidget(btn_none)
|
|
73
|
+
buttons_layout.addStretch()
|
|
74
|
+
buttons_layout.addWidget(btn_ok)
|
|
75
|
+
buttons_layout.addWidget(btn_cancel)
|
|
76
|
+
|
|
77
|
+
main_layout.addLayout(buttons_layout)
|
|
78
|
+
|
|
79
|
+
def _select_all(self):
|
|
80
|
+
for i in range(self.list_behaviors.count()):
|
|
81
|
+
self.list_behaviors.item(i).setSelected(True)
|
|
82
|
+
|
|
83
|
+
def _select_none(self):
|
|
84
|
+
for i in range(self.list_behaviors.count()):
|
|
85
|
+
self.list_behaviors.item(i).setSelected(False)
|
|
86
|
+
|
|
87
|
+
def selected_codes(self):
|
|
88
|
+
return [it.text() for it in self.list_behaviors.selectedItems()]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------
|
|
92
|
+
# Dialog: split videos
|
|
93
|
+
# ---------------------------
|
|
94
|
+
class CategoryDialog(QDialog):
|
|
95
|
+
def __init__(self, items, parent=None):
|
|
96
|
+
super().__init__(parent)
|
|
97
|
+
|
|
98
|
+
self.setWindowTitle("Organize the videos in categories")
|
|
99
|
+
self.setModal(True)
|
|
100
|
+
|
|
101
|
+
main_layout = QVBoxLayout(self)
|
|
102
|
+
lists_layout = QHBoxLayout()
|
|
103
|
+
|
|
104
|
+
self.list_unclassified = self._make_list_widget()
|
|
105
|
+
self.list_train = self._make_list_widget()
|
|
106
|
+
self.list_val = self._make_list_widget()
|
|
107
|
+
self.list_test = self._make_list_widget()
|
|
108
|
+
self.list_inference = self._make_list_widget()
|
|
109
|
+
|
|
110
|
+
lists_layout.addLayout(self._make_column("All videos", self.list_unclassified))
|
|
111
|
+
lists_layout.addLayout(self._make_column("train", self.list_train))
|
|
112
|
+
lists_layout.addLayout(self._make_column("val", self.list_val))
|
|
113
|
+
lists_layout.addLayout(self._make_column("test", self.list_test))
|
|
114
|
+
lists_layout.addLayout(self._make_column("inference", self.list_inference))
|
|
115
|
+
|
|
116
|
+
main_layout.addLayout(lists_layout)
|
|
117
|
+
|
|
118
|
+
buttons_layout = QHBoxLayout()
|
|
119
|
+
btn_ok = QPushButton("OK")
|
|
120
|
+
btn_cancel = QPushButton("Cancel")
|
|
121
|
+
btn_ok.clicked.connect(self.accept)
|
|
122
|
+
btn_cancel.clicked.connect(self.reject)
|
|
123
|
+
|
|
124
|
+
buttons_layout.addStretch()
|
|
125
|
+
buttons_layout.addWidget(btn_ok)
|
|
126
|
+
buttons_layout.addWidget(btn_cancel)
|
|
127
|
+
main_layout.addLayout(buttons_layout)
|
|
128
|
+
|
|
129
|
+
for text in items:
|
|
130
|
+
QListWidgetItem(text, self.list_unclassified)
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def _make_column(title, widget):
|
|
134
|
+
col = QVBoxLayout()
|
|
135
|
+
col.addWidget(QLabel(title))
|
|
136
|
+
col.addWidget(widget)
|
|
137
|
+
return col
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _make_list_widget():
|
|
141
|
+
lw = QListWidget()
|
|
142
|
+
lw.setSelectionMode(QListWidget.ExtendedSelection)
|
|
143
|
+
lw.setDragEnabled(True)
|
|
144
|
+
lw.setAcceptDrops(True)
|
|
145
|
+
lw.setDropIndicatorShown(True)
|
|
146
|
+
lw.setDragDropMode(QListWidget.DragDrop)
|
|
147
|
+
lw.setDefaultDropAction(Qt.MoveAction)
|
|
148
|
+
return lw
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def _collect(widget):
|
|
152
|
+
# "*" is used to mark videos with at least one event
|
|
153
|
+
return [widget.item(i).text().rstrip("*") for i in range(widget.count())]
|
|
154
|
+
|
|
155
|
+
def categories(self):
|
|
156
|
+
return {
|
|
157
|
+
"unclassified": self._collect(self.list_unclassified),
|
|
158
|
+
"train": self._collect(self.list_train),
|
|
159
|
+
"val": self._collect(self.list_val),
|
|
160
|
+
"test": self._collect(self.list_test),
|
|
161
|
+
"inference": self._collect(self.list_inference),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def run(df: pd.DataFrame, project: dict):
|
|
166
|
+
"""Export BORIS observations/events to a FERAL-compatible JSON.
|
|
167
|
+
|
|
168
|
+
See https://www.getferal.ai/ > Label Preparation
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def log(msg):
|
|
172
|
+
messages.append(str(msg))
|
|
173
|
+
|
|
174
|
+
def safe_float(d, key):
|
|
175
|
+
try:
|
|
176
|
+
return float(d[key])
|
|
177
|
+
except Exception:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
messages = []
|
|
181
|
+
|
|
182
|
+
out = {
|
|
183
|
+
"is_multilabel": False,
|
|
184
|
+
"splits": {"train": [], "val": [], "test": [], "inference": []},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# ---- Behaviors (FERAL classes) ----
|
|
188
|
+
behavior_conf = project.get("behaviors_conf", {})
|
|
189
|
+
boris_codes = [behavior_conf[k].get("code") for k in behavior_conf]
|
|
190
|
+
boris_codes = [c for c in boris_codes if c] # drop None/empty
|
|
191
|
+
|
|
192
|
+
# Reserve 0 for background. If BORIS has a behavior literally named "other",
|
|
193
|
+
# treat it as background and do not include as a class.
|
|
194
|
+
boris_codes_no_other = [c for c in boris_codes if c != "other"]
|
|
195
|
+
|
|
196
|
+
dlg = BehaviorSelectDialog(boris_codes_no_other)
|
|
197
|
+
if not dlg.exec():
|
|
198
|
+
return "Behavior selection canceled; export aborted."
|
|
199
|
+
|
|
200
|
+
selected = sorted(set(dlg.selected_codes()))
|
|
201
|
+
if not selected:
|
|
202
|
+
log("No behaviors selected: everything will be mapped to class 0 ('other').")
|
|
203
|
+
|
|
204
|
+
class_names = {"0": "other"}
|
|
205
|
+
for i, code in enumerate(selected, start=1):
|
|
206
|
+
class_names[str(i)] = code
|
|
207
|
+
|
|
208
|
+
out["class_names"] = class_names
|
|
209
|
+
behavior_to_idx = {code: i for i, code in enumerate(selected, start=1)}
|
|
210
|
+
|
|
211
|
+
if selected:
|
|
212
|
+
log(f"Selected behaviors: {', '.join(selected)}")
|
|
213
|
+
log(f"Classes: {class_names}")
|
|
214
|
+
|
|
215
|
+
# ---- Iterate observations/videos ----
|
|
216
|
+
labels = {}
|
|
217
|
+
video_list = []
|
|
218
|
+
|
|
219
|
+
# df dataframe cannot have a "Media file" column
|
|
220
|
+
# has_media_file_col = "Media file" in df.columns
|
|
221
|
+
has_subject_col = "Subject" in df.columns
|
|
222
|
+
|
|
223
|
+
observations = sorted(project.get("observations", {}).keys())
|
|
224
|
+
if not observations:
|
|
225
|
+
return "No observations found in project; nothing to export."
|
|
226
|
+
|
|
227
|
+
for obs_id in observations:
|
|
228
|
+
log("---")
|
|
229
|
+
log(obs_id)
|
|
230
|
+
|
|
231
|
+
obs = project["observations"][obs_id]
|
|
232
|
+
media_files = obs.get("file", {}).get("1", [])
|
|
233
|
+
if not media_files:
|
|
234
|
+
log(f"Observation {obs_id} has no video in player 1.")
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
media_info = obs.get("media_info", {})
|
|
238
|
+
fps_dict = media_info.get("fps", {})
|
|
239
|
+
length_dict = media_info.get("length", {})
|
|
240
|
+
frames_dict = media_info.get("frames", {}) or {}
|
|
241
|
+
|
|
242
|
+
for media_path in media_files:
|
|
243
|
+
video_name = Path(media_path).name
|
|
244
|
+
|
|
245
|
+
if video_name in labels:
|
|
246
|
+
log(f"Duplicate video name '{video_name}' encountered; skipping (obs {obs_id}).")
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
# df dataframe cannot have a "Media file" column
|
|
250
|
+
# Filter events for this observation + this media file when possible
|
|
251
|
+
# if has_media_file_col:
|
|
252
|
+
# df_video = df[(df["Observation id"] == obs_id) & (df["Media file"] == media_path)]
|
|
253
|
+
# else:
|
|
254
|
+
# df_video = df[df["Observation id"] == obs_id]
|
|
255
|
+
# log("Warning: df has no 'Media file' column; using all events from observation.")
|
|
256
|
+
|
|
257
|
+
df_video = df[df["Observation id"] == obs_id]
|
|
258
|
+
|
|
259
|
+
# Enforce single-subject labeling when Subject column exists
|
|
260
|
+
if has_subject_col and not df_video.empty:
|
|
261
|
+
subjects = df_video["Subject"].dropna().unique().tolist()
|
|
262
|
+
if len(subjects) > 1:
|
|
263
|
+
log(f"More than one subject in {video_name}: {subjects}. Skipping.")
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
# Mark videos that contain at least one event with "*"
|
|
267
|
+
video_list.append(video_name + ("*" if not df_video.empty else ""))
|
|
268
|
+
|
|
269
|
+
fps = safe_float(fps_dict, media_path)
|
|
270
|
+
duration = safe_float(length_dict, media_path)
|
|
271
|
+
if fps is None:
|
|
272
|
+
log(f"Missing/invalid FPS for {video_name}. Skipping.")
|
|
273
|
+
continue
|
|
274
|
+
if duration is None:
|
|
275
|
+
log(f"Missing/invalid duration for {video_name}. Skipping.")
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
if media_path in frames_dict:
|
|
279
|
+
n_frames = int(frames_dict[media_path])
|
|
280
|
+
log(f"{video_name}: fps={fps} duration={duration} frames={n_frames} (BORIS)")
|
|
281
|
+
else:
|
|
282
|
+
n_frames = int(round(duration * fps))
|
|
283
|
+
log(f"{video_name}: fps={fps} duration={duration} frames={n_frames} (rounded)")
|
|
284
|
+
|
|
285
|
+
if n_frames <= 0:
|
|
286
|
+
log(f"Non-positive frame count for {video_name}. Skipping.")
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
frame_dt = 1.0 / fps
|
|
290
|
+
labels[video_name] = [0] * n_frames # default: "other"
|
|
291
|
+
|
|
292
|
+
# Fill per-frame labels
|
|
293
|
+
for frame_idx in range(n_frames):
|
|
294
|
+
t = frame_idx * frame_dt
|
|
295
|
+
behaviors = df_video[(df_video["Start (s)"] <= t) & (df_video["Stop (s)"] >= t)]["Behavior"].unique().tolist()
|
|
296
|
+
|
|
297
|
+
if len(behaviors) > 1:
|
|
298
|
+
log(
|
|
299
|
+
f"{video_name}: overlapping behaviors at frame {frame_idx} (t={t:.6f}s): "
|
|
300
|
+
f"{behaviors}. Removing video (is_multilabel=False)."
|
|
301
|
+
)
|
|
302
|
+
del labels[video_name]
|
|
303
|
+
break
|
|
304
|
+
|
|
305
|
+
if not behaviors:
|
|
306
|
+
continue
|
|
307
|
+
|
|
308
|
+
labels[video_name][frame_idx] = behavior_to_idx.get(behaviors[0], 0)
|
|
309
|
+
|
|
310
|
+
out["labels"] = labels
|
|
311
|
+
|
|
312
|
+
# ---- Splits dialog ----
|
|
313
|
+
split_dlg = CategoryDialog(video_list)
|
|
314
|
+
if not split_dlg.exec():
|
|
315
|
+
log("Export canceled at split assignment stage.")
|
|
316
|
+
return "\n".join(messages)
|
|
317
|
+
|
|
318
|
+
splits = split_dlg.categories()
|
|
319
|
+
splits.pop("unclassified", None)
|
|
320
|
+
out["splits"] = splits
|
|
321
|
+
|
|
322
|
+
filename, _ = QFileDialog.getSaveFileName(
|
|
323
|
+
None,
|
|
324
|
+
"Choose a file to save",
|
|
325
|
+
"",
|
|
326
|
+
"JSON files (*.json);;All files (*.*)",
|
|
327
|
+
)
|
|
328
|
+
if not filename:
|
|
329
|
+
log("No output file selected; nothing written.")
|
|
330
|
+
return "\n".join(messages)
|
|
331
|
+
|
|
332
|
+
with open(filename, "w", encoding="utf-8") as f_out:
|
|
333
|
+
json.dump(out, f_out, indent=2)
|
|
334
|
+
|
|
335
|
+
log(f"Saved: {filename}")
|
|
336
|
+
return "\n".join(messages)
|
{boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/irr_weighted_cohen_kappa.py
RENAMED
|
@@ -4,9 +4,9 @@ BORIS plugin
|
|
|
4
4
|
Inter Rater Reliability (IRR) Weighted Cohen's Kappa
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
from typing import List, Tuple, Dict, Optional
|
|
7
|
+
from typing import Dict, List, Optional, Tuple
|
|
9
8
|
|
|
9
|
+
import pandas as pd
|
|
10
10
|
from PySide6.QtWidgets import QInputDialog
|
|
11
11
|
|
|
12
12
|
__version__ = "0.0.3"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This file is part of BORIS.
|
|
7
7
|
|
|
@@ -119,12 +119,12 @@ class BehaviorsMapCreatorWindow(QMainWindow):
|
|
|
119
119
|
self.saveMapAction.setShortcut("Ctrl+S")
|
|
120
120
|
self.saveMapAction.setStatusTip("Save the behavior coding map")
|
|
121
121
|
self.saveMapAction.setEnabled(False)
|
|
122
|
-
self.saveMapAction.triggered.connect(self.
|
|
122
|
+
self.saveMapAction.triggered.connect(self.save_map_clicked)
|
|
123
123
|
|
|
124
124
|
self.saveAsMapAction = QAction(QIcon(), "Save the behavior coding map as ...", self)
|
|
125
125
|
self.saveAsMapAction.setStatusTip("Save the behavior coding map as ...")
|
|
126
126
|
self.saveAsMapAction.setEnabled(False)
|
|
127
|
-
self.saveAsMapAction.triggered.connect(self.
|
|
127
|
+
self.saveAsMapAction.triggered.connect(self.save_as_map_clicked)
|
|
128
128
|
|
|
129
129
|
self.mapNameAction = QAction(QIcon(), "&Edit name of behaviors coding map", self)
|
|
130
130
|
self.mapNameAction.setShortcut("Ctrl+M")
|
|
@@ -389,7 +389,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
|
|
|
389
389
|
)
|
|
390
390
|
|
|
391
391
|
if response == cfg.SAVE:
|
|
392
|
-
if not self.
|
|
392
|
+
if not self.save_map_clicked():
|
|
393
393
|
event.ignore()
|
|
394
394
|
|
|
395
395
|
if response == cfg.CANCEL:
|
|
@@ -615,7 +615,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
|
|
|
615
615
|
)
|
|
616
616
|
|
|
617
617
|
if response == cfg.SAVE:
|
|
618
|
-
if not self.
|
|
618
|
+
if not self.save_map_clicked():
|
|
619
619
|
return
|
|
620
620
|
|
|
621
621
|
if response == cfg.CANCEL:
|
|
@@ -659,7 +659,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
|
|
|
659
659
|
["Save", "Discard", "Cancel"],
|
|
660
660
|
)
|
|
661
661
|
|
|
662
|
-
if (response == "Save" and not self.
|
|
662
|
+
if (response == "Save" and not self.save_map_clicked()) or (response == "Cancel"):
|
|
663
663
|
return
|
|
664
664
|
|
|
665
665
|
fileName, _ = QFileDialog(self).getOpenFileName(
|
|
@@ -783,7 +783,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
|
|
|
783
783
|
else:
|
|
784
784
|
return False
|
|
785
785
|
|
|
786
|
-
def
|
|
786
|
+
def save_as_map_clicked(self):
|
|
787
787
|
filters = "Behaviors coding map (*.behav_coding_map);;All files (*)"
|
|
788
788
|
|
|
789
789
|
self.fileName, _ = QFileDialog.getSaveFileName(self, "Save behaviors coding map as", "", filters)
|
|
@@ -794,7 +794,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
|
|
|
794
794
|
self.fileName += ".behav_coding_map"
|
|
795
795
|
self.saveMap()
|
|
796
796
|
|
|
797
|
-
def
|
|
797
|
+
def save_map_clicked(self):
|
|
798
798
|
if not self.fileName:
|
|
799
799
|
self.fileName, _ = QFileDialog().getSaveFileName(
|
|
800
800
|
self,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This program is free software; you can redistribute it and/or modify
|
|
7
7
|
it under the terms of the GNU General Public License as published by
|
|
@@ -3,7 +3,7 @@ BORIS CLI
|
|
|
3
3
|
|
|
4
4
|
Behavioral Observation Research Interactive Software Command Line Interface
|
|
5
5
|
|
|
6
|
-
Copyright 2012-
|
|
6
|
+
Copyright 2012-2026 Olivier Friard
|
|
7
7
|
|
|
8
8
|
This program is free software; you can redistribute it and/or modify
|
|
9
9
|
it under the terms of the GNU General Public License as published by
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This program is free software; you can redistribute it and/or modify
|
|
7
7
|
it under the terms of the GNU General Public License as published by
|
|
@@ -38,7 +38,7 @@ class Button(QWidget):
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
class CodingPad(QWidget):
|
|
41
|
-
|
|
41
|
+
click_signal = Signal(str)
|
|
42
42
|
sendEventSignal = Signal(QEvent)
|
|
43
43
|
close_signal = Signal(QRect, dict)
|
|
44
44
|
|
|
@@ -208,7 +208,8 @@ class CodingPad(QWidget):
|
|
|
208
208
|
"""
|
|
209
209
|
Button clicked
|
|
210
210
|
"""
|
|
211
|
-
|
|
211
|
+
print(f"{behavior_code=}")
|
|
212
|
+
self.click_signal.emit(behavior_code)
|
|
212
213
|
|
|
213
214
|
def eventFilter(self, receiver, event) -> bool:
|
|
214
215
|
"""
|
|
@@ -261,7 +262,7 @@ def show_coding_pad(self):
|
|
|
261
262
|
self.codingpad.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
262
263
|
self.codingpad.sendEventSignal.connect(self.signal_from_widget)
|
|
263
264
|
|
|
264
|
-
self.codingpad.
|
|
265
|
+
self.codingpad.click_signal.connect(self.click_signal_from_coding_pad)
|
|
265
266
|
self.codingpad.close_signal.connect(self.close_signal_from_coding_pad)
|
|
266
267
|
self.codingpad.show()
|
|
267
268
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2026 Olivier Friard
|
|
5
5
|
|
|
6
6
|
This file is part of BORIS.
|
|
7
7
|
|
|
@@ -165,6 +165,7 @@ REMOVE = "Remove"
|
|
|
165
165
|
SAVE = "Save"
|
|
166
166
|
DISCARD = "Discard"
|
|
167
167
|
OK = "OK"
|
|
168
|
+
ABORT = "Abort"
|
|
168
169
|
OVERWRITE = "Overwrite"
|
|
169
170
|
OVERWRITE_ALL = "Overwrite all"
|
|
170
171
|
SKIP = "Skip"
|
|
@@ -493,6 +494,13 @@ FRAME_DEFAULT_CACHE_SIZE = 1
|
|
|
493
494
|
|
|
494
495
|
EXCLUDED = "excluded"
|
|
495
496
|
|
|
497
|
+
# codes for Input_dialog class
|
|
498
|
+
CHECKBOX = "cb"
|
|
499
|
+
LINE_EDIT = "le"
|
|
500
|
+
SPINBOX = "sb"
|
|
501
|
+
DOUBLE_SPINBOX = "dsb"
|
|
502
|
+
ITEMS_LIST = "il"
|
|
503
|
+
|
|
496
504
|
# modifiers
|
|
497
505
|
MODIFIERS = "modifiers"
|
|
498
506
|
SINGLE_SELECTION = 0
|
|
@@ -539,10 +547,13 @@ SPECTROGRAM_DEFAULT_TIME_INTERVAL = 10
|
|
|
539
547
|
SPECTROGRAM_WINDOW_TYPE = "SPECTROGRAM_WINDOW_TYPE"
|
|
540
548
|
SPECTROGRAM_DEFAULT_WINDOW_TYPE = "hanning"
|
|
541
549
|
SPECTROGRAM_NFFT = "SPECTROGRAM_NFFT"
|
|
542
|
-
SPECTROGRAM_DEFAULT_NFFT = "
|
|
550
|
+
SPECTROGRAM_DEFAULT_NFFT = "256"
|
|
543
551
|
SPECTROGRAM_NOVERLAP = "SPECTROGRAM_NOVERLAP"
|
|
544
|
-
SPECTROGRAM_DEFAULT_NOVERLAP =
|
|
552
|
+
SPECTROGRAM_DEFAULT_NOVERLAP = 128
|
|
545
553
|
SPECTROGRAM_VMIN = "SPECTROGRAM_VMIN"
|
|
554
|
+
|
|
555
|
+
SPECTROGRAM_USE_VMIN_VMAX = "SPECTROGRAM_USE_VMIN_VMAX"
|
|
556
|
+
SPECTROGRAM_USE_VMIN_VMAX_DEFAULT = False
|
|
546
557
|
SPECTROGRAM_DEFAULT_VMIN = -100
|
|
547
558
|
SPECTROGRAM_VMAX = "SPECTROGRAM_VMAX"
|
|
548
559
|
SPECTROGRAM_DEFAULT_VMAX = -20
|
|
@@ -737,6 +748,7 @@ INIT_PARAM = {
|
|
|
737
748
|
PROJECT_FILE_INDENTATION: PROJECT_FILE_INDENTATION_DEFAULT_VALUE,
|
|
738
749
|
f"{MEDIA} tw fields": MEDIA_TW_EVENTS_FIELDS_DEFAULT,
|
|
739
750
|
# FRAME_STEP_SIZE: FRAME_STEP_SIZE_DEFAULT_VALUE,
|
|
751
|
+
TOOLBAR_ICON_SIZE: DEFAULT_TOOLBAR_ICON_SIZE_VALUE,
|
|
740
752
|
}
|
|
741
753
|
|
|
742
754
|
SDIS_EXT = "sds"
|