boris-behav-obs 9.3.2__py2.py3-none-any.whl → 9.3.4__py2.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/config.py +4 -1
- boris/core.py +0 -5
- boris/core_qrc.py +6580 -6784
- boris/dialog.py +74 -5
- boris/event_operations.py +14 -2
- boris/media_file.py +12 -10
- boris/observation_operations.py +21 -12
- boris/project.py +78 -28
- boris/project_functions.py +38 -14
- boris/project_import_export.py +3 -1
- boris/utilities.py +1 -2
- boris/version.py +2 -2
- {boris_behav_obs-9.3.2.dist-info → boris_behav_obs-9.3.4.dist-info}/METADATA +2 -2
- {boris_behav_obs-9.3.2.dist-info → boris_behav_obs-9.3.4.dist-info}/RECORD +18 -18
- {boris_behav_obs-9.3.2.dist-info → boris_behav_obs-9.3.4.dist-info}/WHEEL +1 -1
- {boris_behav_obs-9.3.2.dist-info → boris_behav_obs-9.3.4.dist-info}/entry_points.txt +0 -0
- {boris_behav_obs-9.3.2.dist-info → boris_behav_obs-9.3.4.dist-info}/licenses/LICENSE.TXT +0 -0
- {boris_behav_obs-9.3.2.dist-info → boris_behav_obs-9.3.4.dist-info}/top_level.txt +0 -0
boris/dialog.py
CHANGED
|
@@ -88,7 +88,7 @@ def MessageDialog(title: str, text: str, buttons: tuple) -> str:
|
|
|
88
88
|
for button in buttons:
|
|
89
89
|
message.addButton(button, QMessageBox.YesRole)
|
|
90
90
|
|
|
91
|
-
message.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
91
|
+
message.setWindowFlags(message.windowFlags() | Qt.WindowStaysOnTopHint)
|
|
92
92
|
message.exec()
|
|
93
93
|
return message.clickedButton().text()
|
|
94
94
|
|
|
@@ -184,7 +184,7 @@ class Info_widget(QWidget):
|
|
|
184
184
|
|
|
185
185
|
class get_time_widget(QWidget):
|
|
186
186
|
"""
|
|
187
|
-
widget for selecting a time in various formats:
|
|
187
|
+
widget for selecting a time in various formats: seconds, HH:MM:SS:ZZZ or YYYY-mm-DD HH:MM:SS:ZZZ
|
|
188
188
|
"""
|
|
189
189
|
|
|
190
190
|
def __init__(self, time_value=dec(0), parent=None):
|
|
@@ -408,7 +408,7 @@ class get_time_widget(QWidget):
|
|
|
408
408
|
if self.pb_sign.text() == "-":
|
|
409
409
|
time_sec = -time_sec
|
|
410
410
|
|
|
411
|
-
return dec(time_sec) if time_sec is not None else None
|
|
411
|
+
return dec(time_sec).quantize(dec("0.001")) if time_sec is not None else None
|
|
412
412
|
|
|
413
413
|
def set_time(self, new_time: dec) -> None:
|
|
414
414
|
"""
|
|
@@ -440,6 +440,11 @@ class get_time_widget(QWidget):
|
|
|
440
440
|
|
|
441
441
|
|
|
442
442
|
class Ask_time(QDialog):
|
|
443
|
+
"""
|
|
444
|
+
Qdialog class for asking time to user
|
|
445
|
+
User can select a time format between seconds, HHMMSS.zzz or YYY-mm-DD HH:MM:SS.zzz
|
|
446
|
+
"""
|
|
447
|
+
|
|
443
448
|
def __init__(self, time_value=0):
|
|
444
449
|
super().__init__()
|
|
445
450
|
self.setWindowTitle("")
|
|
@@ -453,10 +458,10 @@ class Ask_time(QDialog):
|
|
|
453
458
|
|
|
454
459
|
hbox.addWidget(self.time_widget)
|
|
455
460
|
|
|
456
|
-
self.pbOK = QPushButton(
|
|
461
|
+
self.pbOK = QPushButton(cfg.OK, clicked=self.pb_ok_clicked)
|
|
457
462
|
self.pbOK.setDefault(True)
|
|
458
463
|
|
|
459
|
-
self.pbCancel = QPushButton(
|
|
464
|
+
self.pbCancel = QPushButton(cfg.CANCEL)
|
|
460
465
|
self.pbCancel.clicked.connect(self.reject)
|
|
461
466
|
|
|
462
467
|
self.hbox2 = QHBoxLayout(self)
|
|
@@ -975,6 +980,70 @@ class Results_dialog(QDialog):
|
|
|
975
980
|
self.done(cfg.SAVE_DATASET)
|
|
976
981
|
|
|
977
982
|
|
|
983
|
+
class Results_dialog_exit_code(QDialog):
|
|
984
|
+
"""
|
|
985
|
+
widget for visualizing text output
|
|
986
|
+
"""
|
|
987
|
+
|
|
988
|
+
def __init__(self):
|
|
989
|
+
super().__init__()
|
|
990
|
+
|
|
991
|
+
self.dataset = False
|
|
992
|
+
|
|
993
|
+
self.setWindowTitle("")
|
|
994
|
+
|
|
995
|
+
hbox = QVBoxLayout()
|
|
996
|
+
|
|
997
|
+
self.lb = QLabel("")
|
|
998
|
+
hbox.addWidget(self.lb)
|
|
999
|
+
|
|
1000
|
+
self.ptText = QPlainTextEdit()
|
|
1001
|
+
self.ptText.setReadOnly(True)
|
|
1002
|
+
hbox.addWidget(self.ptText)
|
|
1003
|
+
|
|
1004
|
+
hbox2 = QHBoxLayout()
|
|
1005
|
+
hbox2.addItem(QSpacerItem(241, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
|
1006
|
+
|
|
1007
|
+
self.pbSave = QPushButton("Save results", clicked=self.save_results)
|
|
1008
|
+
hbox2.addWidget(self.pbSave)
|
|
1009
|
+
|
|
1010
|
+
self.pb1 = QPushButton("1", clicked=lambda: self.done_(1))
|
|
1011
|
+
hbox2.addWidget(self.pb1)
|
|
1012
|
+
|
|
1013
|
+
self.pb2 = QPushButton("2", clicked=lambda: self.done_(2))
|
|
1014
|
+
hbox2.addWidget(self.pb2)
|
|
1015
|
+
|
|
1016
|
+
self.pb3 = QPushButton("3", clicked=lambda: self.done_(3))
|
|
1017
|
+
hbox2.addWidget(self.pb3)
|
|
1018
|
+
|
|
1019
|
+
hbox.addLayout(hbox2)
|
|
1020
|
+
self.setLayout(hbox)
|
|
1021
|
+
|
|
1022
|
+
self.resize(800, 640)
|
|
1023
|
+
|
|
1024
|
+
def done_(self, status):
|
|
1025
|
+
self.done(status)
|
|
1026
|
+
|
|
1027
|
+
def save_results(self):
|
|
1028
|
+
"""
|
|
1029
|
+
save content of self.ptText
|
|
1030
|
+
"""
|
|
1031
|
+
|
|
1032
|
+
if not self.dataset:
|
|
1033
|
+
file_name, _ = QFileDialog().getSaveFileName(self, "Save results", "", "Text files (*.txt *.tsv);;All files (*)")
|
|
1034
|
+
|
|
1035
|
+
if not file_name:
|
|
1036
|
+
return
|
|
1037
|
+
try:
|
|
1038
|
+
with open(file_name, "w") as f:
|
|
1039
|
+
f.write(self.ptText.toPlainText())
|
|
1040
|
+
except Exception:
|
|
1041
|
+
QMessageBox.critical(self, cfg.programName, f"The file {file_name} can not be saved")
|
|
1042
|
+
|
|
1043
|
+
else:
|
|
1044
|
+
self.done(cfg.SAVE_DATASET)
|
|
1045
|
+
|
|
1046
|
+
|
|
978
1047
|
class View_data(QDialog):
|
|
979
1048
|
"""
|
|
980
1049
|
widget for visualizing rows of data file
|
boris/event_operations.py
CHANGED
|
@@ -576,6 +576,8 @@ def edit_event(self):
|
|
|
576
576
|
|
|
577
577
|
pj_event_idx = self.tv_idx2events_idx[tvevents_row]
|
|
578
578
|
|
|
579
|
+
print(f"{self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]=}")
|
|
580
|
+
|
|
579
581
|
time_value = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
|
|
580
582
|
cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.TIME]
|
|
581
583
|
]
|
|
@@ -601,9 +603,12 @@ def edit_event(self):
|
|
|
601
603
|
)
|
|
602
604
|
edit_window.setWindowTitle("Edit event")
|
|
603
605
|
|
|
606
|
+
print(f"{time_value=}")
|
|
607
|
+
|
|
604
608
|
# time
|
|
605
609
|
if time_value.is_nan():
|
|
606
610
|
edit_window.cb_set_time_na.setChecked(True)
|
|
611
|
+
|
|
607
612
|
if self.playerType in (cfg.VIEWER_MEDIA, cfg.VIEWER_LIVE, cfg.VIEWER_IMAGES):
|
|
608
613
|
edit_window.pb_set_to_current_time.setVisible(False)
|
|
609
614
|
|
|
@@ -673,12 +678,15 @@ def edit_event(self):
|
|
|
673
678
|
|
|
674
679
|
flag_ok = False # for looping until event is OK or Cancel pressed
|
|
675
680
|
while True:
|
|
676
|
-
if edit_window.
|
|
681
|
+
if edit_window.exec(): # button OK
|
|
677
682
|
self.project_changed()
|
|
678
683
|
|
|
679
684
|
# MEDIA / LIVE
|
|
680
685
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
|
|
681
686
|
new_time = edit_window.time_widget.get_time()
|
|
687
|
+
|
|
688
|
+
print(f"{new_time=}")
|
|
689
|
+
|
|
682
690
|
if new_time is None:
|
|
683
691
|
QMessageBox.warning(
|
|
684
692
|
self,
|
|
@@ -747,7 +755,11 @@ def edit_event(self):
|
|
|
747
755
|
for key in self.pj[cfg.ETHOGRAM]:
|
|
748
756
|
if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
|
|
749
757
|
event = self.full_event(key)
|
|
750
|
-
if
|
|
758
|
+
if (
|
|
759
|
+
edit_window.time_value == cfg.NA
|
|
760
|
+
or (isinstance(edit_window.time_value, dec) and edit_window.time_value.is_nan())
|
|
761
|
+
or (edit_window.cb_set_time_na.isChecked())
|
|
762
|
+
):
|
|
751
763
|
event[cfg.TIME] = dec("NaN")
|
|
752
764
|
else:
|
|
753
765
|
event[cfg.TIME] = edit_window.time_widget.get_time()
|
boris/media_file.py
CHANGED
|
@@ -20,11 +20,13 @@ This file is part of BORIS.
|
|
|
20
20
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
+
from PySide6.QtWidgets import QFileDialog
|
|
24
|
+
|
|
23
25
|
from . import config as cfg
|
|
24
26
|
from . import utilities as util
|
|
25
27
|
from . import dialog
|
|
26
28
|
from . import project_functions
|
|
27
|
-
from
|
|
29
|
+
from . import utilities as util
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
def get_info(self) -> None:
|
|
@@ -38,17 +40,17 @@ def get_info(self) -> None:
|
|
|
38
40
|
if "error" in r:
|
|
39
41
|
ffmpeg_output = f"File path: {media_full_path}<br><br>{r['error']}<br><br>"
|
|
40
42
|
else:
|
|
41
|
-
ffmpeg_output = f"<br><b>{r['analysis_program']
|
|
43
|
+
ffmpeg_output = f"<br><b>{r['analysis_program']} analysis</b><br>"
|
|
42
44
|
|
|
43
45
|
ffmpeg_output += (
|
|
44
46
|
f"File path: <b>{media_full_path}</b><br><br>"
|
|
45
47
|
f"Duration: {r['duration']} seconds ({util.convertTime(self.timeFormat, r['duration'])})<br>"
|
|
48
|
+
f"FPS: {r['fps']}<br>"
|
|
49
|
+
f"Resolution: {r['resolution']} pixels<br>"
|
|
46
50
|
f"Format long name: {r.get('format_long_name', cfg.NA)}<br>"
|
|
47
51
|
f"Creation time: {r.get('creation_time', cfg.NA)}<br>"
|
|
48
|
-
f"Resolution: {r['resolution']}<br>"
|
|
49
52
|
f"Number of frames: {r['frames_number']}<br>"
|
|
50
53
|
f"Bitrate: {util.smart_size_format(r['bitrate'])} <br>"
|
|
51
|
-
f"FPS: {r['fps']}<br>"
|
|
52
54
|
f"Has video: {r['has_video']}<br>"
|
|
53
55
|
f"Has audio: {r['has_audio']}<br>"
|
|
54
56
|
f"File size: {util.smart_size_format(r.get('file size', cfg.NA))}<br>"
|
|
@@ -70,6 +72,12 @@ def get_info(self) -> None:
|
|
|
70
72
|
|
|
71
73
|
mpv_output = (
|
|
72
74
|
"<b>MPV information</b><br>"
|
|
75
|
+
f"Duration: {dw.player.duration} seconds ({util.seconds2time(dw.player.duration)})<br>"
|
|
76
|
+
# "Position: {} %<br>"
|
|
77
|
+
f"FPS: {dw.player.container_fps}<br>"
|
|
78
|
+
# "Rate: {}<br>"
|
|
79
|
+
f"Resolution: {dw.player.width}x{dw.player.height} pixels<br>"
|
|
80
|
+
# "Scale: {}<br>"
|
|
73
81
|
f"Video format: {dw.player.video_format}<br>"
|
|
74
82
|
# "State: {}<br>"
|
|
75
83
|
# "Media Resource Location: {}<br>"
|
|
@@ -77,12 +85,6 @@ def get_info(self) -> None:
|
|
|
77
85
|
# "Track: {}/{}<br>"
|
|
78
86
|
f"Number of media in media list: {dw.player.playlist_count}<br>"
|
|
79
87
|
f"Current time position: {dw.player.time_pos}<br>"
|
|
80
|
-
f"Duration: {dw.player.duration}<br>"
|
|
81
|
-
# "Position: {} %<br>"
|
|
82
|
-
f"FPS: {dw.player.container_fps}<br>"
|
|
83
|
-
# "Rate: {}<br>"
|
|
84
|
-
f"Video size: {dw.player.width}x{dw.player.height}<br>"
|
|
85
|
-
# "Scale: {}<br>"
|
|
86
88
|
f"Aspect ratio: {round(dw.player.width / dw.player.height, 3)}<br>"
|
|
87
89
|
# "is seekable? {}<br>"
|
|
88
90
|
# "has_vout? {}<br>"
|
boris/observation_operations.py
CHANGED
|
@@ -1100,7 +1100,7 @@ def close_observation(self):
|
|
|
1100
1100
|
|
|
1101
1101
|
logging.info(f"Close observation (player type: {self.playerType})")
|
|
1102
1102
|
|
|
1103
|
-
# check observation events
|
|
1103
|
+
# check observation state events
|
|
1104
1104
|
|
|
1105
1105
|
flag_ok, msg = project_functions.check_state_events_obs(
|
|
1106
1106
|
self.observationId,
|
|
@@ -1111,16 +1111,22 @@ def close_observation(self):
|
|
|
1111
1111
|
|
|
1112
1112
|
if not flag_ok:
|
|
1113
1113
|
out = f"The current observation has state event(s) that are not PAIRED:<br><br>{msg}"
|
|
1114
|
-
results = dialog.
|
|
1114
|
+
results = dialog.Results_dialog_exit_code()
|
|
1115
1115
|
results.setWindowTitle(f"{cfg.programName} - Check selected observations")
|
|
1116
1116
|
results.ptText.setReadOnly(True)
|
|
1117
1117
|
results.ptText.appendHtml(out)
|
|
1118
|
-
results.pbSave.setVisible(False)
|
|
1119
|
-
results.pbCancel.setText("Close observation")
|
|
1120
|
-
results.pbCancel.setVisible(True)
|
|
1121
|
-
results.pbOK.setText("Fix unpaired state events")
|
|
1122
1118
|
|
|
1123
|
-
|
|
1119
|
+
results.pb1.setText("Close observation")
|
|
1120
|
+
results.pb2.setText("Return to observation")
|
|
1121
|
+
if self.playerType == cfg.IMAGES:
|
|
1122
|
+
results.pb3.setVisible(False)
|
|
1123
|
+
else:
|
|
1124
|
+
results.pb3.setText("Fix unpaired state events")
|
|
1125
|
+
|
|
1126
|
+
r = results.exec()
|
|
1127
|
+
if r == 2: # Return to observation
|
|
1128
|
+
return
|
|
1129
|
+
if r == 3: # Fix unpaired state events
|
|
1124
1130
|
state_events.fix_unpaired_events(self, silent_mode=True)
|
|
1125
1131
|
|
|
1126
1132
|
self.saved_state = self.saveState()
|
|
@@ -2406,15 +2412,18 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
|
|
|
2406
2412
|
"""
|
|
2407
2413
|
|
|
2408
2414
|
cumul_media_durations: list = [dec(0)]
|
|
2409
|
-
for media_file in observation[cfg.FILE][
|
|
2410
|
-
|
|
2411
|
-
|
|
2415
|
+
for media_file in observation[cfg.FILE][cfg.PLAYER1]:
|
|
2416
|
+
try:
|
|
2417
|
+
media_duration = dec(str(observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]))
|
|
2418
|
+
cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
|
|
2419
|
+
except KeyError:
|
|
2420
|
+
return None
|
|
2412
2421
|
|
|
2413
2422
|
cumul_media_durations.remove(dec(0))
|
|
2414
2423
|
|
|
2415
2424
|
# test if timestamp is at end of last media
|
|
2416
2425
|
if timestamp == cumul_media_durations[-1]:
|
|
2417
|
-
player_idx = len(observation[cfg.FILE][
|
|
2426
|
+
player_idx = len(observation[cfg.FILE][cfg.PLAYER1]) - 1
|
|
2418
2427
|
else:
|
|
2419
2428
|
player_idx = -1
|
|
2420
2429
|
for idx, value in enumerate(cumul_media_durations):
|
|
@@ -2424,7 +2433,7 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
|
|
|
2424
2433
|
break
|
|
2425
2434
|
|
|
2426
2435
|
if player_idx != -1:
|
|
2427
|
-
video_file_name = observation[cfg.FILE][
|
|
2436
|
+
video_file_name = observation[cfg.FILE][cfg.PLAYER1][player_idx]
|
|
2428
2437
|
else:
|
|
2429
2438
|
video_file_name = None
|
|
2430
2439
|
|
boris/project.py
CHANGED
|
@@ -82,12 +82,12 @@ class BehavioralCategories(QDialog):
|
|
|
82
82
|
# add categories
|
|
83
83
|
self.lw.setColumnCount(2)
|
|
84
84
|
self.lw.setHorizontalHeaderLabels(["Category name", "Color"])
|
|
85
|
-
# self.lw.verticalHeader().hide()
|
|
86
85
|
self.lw.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
|
87
86
|
|
|
88
|
-
# self.lw.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
89
87
|
self.lw.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
90
88
|
|
|
89
|
+
behavioral_categories: list = []
|
|
90
|
+
|
|
91
91
|
if cfg.BEHAVIORAL_CATEGORIES_CONF in pj:
|
|
92
92
|
self.lw.setRowCount(len(pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {})))
|
|
93
93
|
behav_cat = pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {})
|
|
@@ -95,7 +95,7 @@ class BehavioralCategories(QDialog):
|
|
|
95
95
|
# name
|
|
96
96
|
item = QTableWidgetItem()
|
|
97
97
|
item.setText(behav_cat[key]["name"])
|
|
98
|
-
|
|
98
|
+
behavioral_categories.append(behav_cat[key]["name"])
|
|
99
99
|
self.lw.setItem(idx, 0, item)
|
|
100
100
|
# color
|
|
101
101
|
item = QTableWidgetItem()
|
|
@@ -103,24 +103,21 @@ class BehavioralCategories(QDialog):
|
|
|
103
103
|
if behav_cat[key].get(cfg.COLOR, ""):
|
|
104
104
|
item.setBackground(QColor(behav_cat[key].get(cfg.COLOR, "")))
|
|
105
105
|
else:
|
|
106
|
-
# item.setBackground(QColor(230, 230, 230))
|
|
107
106
|
item.setBackground(self.not_editable_column_color())
|
|
108
|
-
# item.setFlags(Qt.ItemIsEnabled)
|
|
109
107
|
self.lw.setItem(idx, 1, item)
|
|
110
108
|
else:
|
|
111
109
|
self.lw.setRowCount(len(pj.get(cfg.BEHAVIORAL_CATEGORIES, [])))
|
|
112
110
|
for idx, category in enumerate(sorted(pj.get(cfg.BEHAVIORAL_CATEGORIES, []))):
|
|
111
|
+
# name
|
|
113
112
|
item = QTableWidgetItem()
|
|
114
113
|
item.setText(category)
|
|
115
|
-
|
|
114
|
+
behavioral_categories.append(category)
|
|
116
115
|
self.lw.setItem(idx, 0, item)
|
|
117
|
-
|
|
116
|
+
# color
|
|
118
117
|
item = QTableWidgetItem()
|
|
119
118
|
item.setText("")
|
|
120
|
-
# item.setFlags(Qt.ItemIsEnabled)
|
|
121
|
-
self.lw.setItem(idx, 1, item)
|
|
122
119
|
|
|
123
|
-
|
|
120
|
+
self.lw.setItem(idx, 1, item)
|
|
124
121
|
|
|
125
122
|
self.vbox.addWidget(self.lw)
|
|
126
123
|
|
|
@@ -148,6 +145,43 @@ class BehavioralCategories(QDialog):
|
|
|
148
145
|
|
|
149
146
|
self.setLayout(self.vbox)
|
|
150
147
|
|
|
148
|
+
# check if behavioral categories are present in events
|
|
149
|
+
behavioral_categories_in_ethogram = set(
|
|
150
|
+
sorted([pj[cfg.ETHOGRAM][idx].get(cfg.BEHAVIOR_CATEGORY, "") for idx in pj.get(cfg.ETHOGRAM, {})])
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if behavioral_categories_in_ethogram.difference(set(behavioral_categories)) and behavioral_categories_in_ethogram.difference(
|
|
154
|
+
set(behavioral_categories)
|
|
155
|
+
) != {""}:
|
|
156
|
+
if (
|
|
157
|
+
dialog.MessageDialog(
|
|
158
|
+
cfg.programName,
|
|
159
|
+
(
|
|
160
|
+
"There are behavioral categories that are present in ethogram but not defined.<br>"
|
|
161
|
+
f"{behavioral_categories_in_ethogram.difference(set(behavioral_categories))}<br>"
|
|
162
|
+
"<br>"
|
|
163
|
+
"Do you want to add them in the behavioral categories list?"
|
|
164
|
+
),
|
|
165
|
+
[cfg.YES, cfg.NO],
|
|
166
|
+
)
|
|
167
|
+
== cfg.YES
|
|
168
|
+
):
|
|
169
|
+
# add behavioral categories present in ethogram in behavioal categories list
|
|
170
|
+
rc = self.lw.rowCount()
|
|
171
|
+
self.lw.setRowCount(rc + len(behavioral_categories_in_ethogram.difference(set(behavioral_categories))))
|
|
172
|
+
for idx, category in enumerate(sorted(list(behavioral_categories_in_ethogram.difference(set(behavioral_categories))))):
|
|
173
|
+
print(category)
|
|
174
|
+
# name
|
|
175
|
+
item = QTableWidgetItem()
|
|
176
|
+
item.setText(category)
|
|
177
|
+
# behavioral_categories.append(category)
|
|
178
|
+
self.lw.setItem(rc + idx, 0, item)
|
|
179
|
+
# color
|
|
180
|
+
item = QTableWidgetItem()
|
|
181
|
+
item.setText("")
|
|
182
|
+
|
|
183
|
+
self.lw.setItem(rc + idx, 1, item)
|
|
184
|
+
|
|
151
185
|
def not_editable_column_color(self):
|
|
152
186
|
"""
|
|
153
187
|
return a color for the not editable column
|
|
@@ -178,7 +212,6 @@ class BehavioralCategories(QDialog):
|
|
|
178
212
|
color = col_diag.currentColor()
|
|
179
213
|
if color.name() == "#000000": # black -> delete color
|
|
180
214
|
self.lw.item(row, 1).setText("")
|
|
181
|
-
# self.lw.item(row, 1).setBackground(QColor(230, 230, 230))
|
|
182
215
|
self.lw.item(row, 1).setBackground(self.not_editable_column_color())
|
|
183
216
|
elif color.isValid():
|
|
184
217
|
self.lw.item(row, 1).setText(color.name())
|
|
@@ -663,7 +696,7 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
663
696
|
QMessageBox.warning(
|
|
664
697
|
self,
|
|
665
698
|
cfg.programName,
|
|
666
|
-
("The following behavior{} are not defined in the ethogram:<br>
|
|
699
|
+
("The following behavior{} are not defined in the ethogram:<br>{}").format(
|
|
667
700
|
"s" if len(bcm_code_not_found) > 1 else "", ",".join(bcm_code_not_found)
|
|
668
701
|
),
|
|
669
702
|
)
|
|
@@ -733,7 +766,7 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
733
766
|
behavioral categories manager
|
|
734
767
|
"""
|
|
735
768
|
|
|
736
|
-
bc = BehavioralCategories(self.pj)
|
|
769
|
+
bc = BehavioralCategories(self.pj)
|
|
737
770
|
|
|
738
771
|
if bc.exec_():
|
|
739
772
|
self.pj[cfg.BEHAVIORAL_CATEGORIES] = []
|
|
@@ -788,18 +821,18 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
788
821
|
column (int): column double-clicked
|
|
789
822
|
"""
|
|
790
823
|
|
|
791
|
-
#
|
|
824
|
+
# excluded column
|
|
792
825
|
if column == cfg.behavioursFields[cfg.EXCLUDED]:
|
|
793
826
|
self.exclusion_matrix()
|
|
794
827
|
|
|
795
|
-
#
|
|
828
|
+
# coding map
|
|
796
829
|
if column == cfg.behavioursFields[cfg.CODING_MAP_sp]:
|
|
797
830
|
if "with coding map" in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
|
|
798
831
|
self.behavior_type_changed(row)
|
|
799
832
|
else:
|
|
800
833
|
QMessageBox.information(self, cfg.programName, "Change the behavior type on first column to select a coding map")
|
|
801
834
|
|
|
802
|
-
#
|
|
835
|
+
# behavior type
|
|
803
836
|
if column == cfg.behavioursFields["type"]:
|
|
804
837
|
self.behavior_type_doubleclicked(row)
|
|
805
838
|
|
|
@@ -811,7 +844,7 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
811
844
|
if column == cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]:
|
|
812
845
|
self.category_doubleclicked(row)
|
|
813
846
|
|
|
814
|
-
#
|
|
847
|
+
# modifiers
|
|
815
848
|
if column == cfg.behavioursFields[cfg.MODIFIERS]:
|
|
816
849
|
# check if behavior has coding map
|
|
817
850
|
if (
|
|
@@ -865,16 +898,17 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
865
898
|
if self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).text():
|
|
866
899
|
current_color = QColor(self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).text())
|
|
867
900
|
if current_color.isValid():
|
|
901
|
+
print(f"{current_color=}")
|
|
868
902
|
col_diag.setCurrentColor(current_color)
|
|
869
903
|
|
|
870
|
-
if col_diag.
|
|
904
|
+
if col_diag.exec():
|
|
871
905
|
color = col_diag.currentColor()
|
|
872
906
|
if color.name() == "#000000": # black -> delete color
|
|
873
907
|
self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText("")
|
|
874
908
|
self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(self.not_editable_column_color())
|
|
875
909
|
elif color.isValid():
|
|
910
|
+
self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(QColor(color.name()))
|
|
876
911
|
self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText(color.name())
|
|
877
|
-
self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(color)
|
|
878
912
|
|
|
879
913
|
def category_doubleclicked(self, row):
|
|
880
914
|
"""
|
|
@@ -1377,7 +1411,7 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1377
1411
|
# let user select a coding maop
|
|
1378
1412
|
file_name, _ = QFileDialog().getOpenFileName(
|
|
1379
1413
|
self,
|
|
1380
|
-
"Select a modifier coding map for
|
|
1414
|
+
f"Select a modifier coding map for {self.twBehaviors.item(row, cfg.behavioursFields['code']).text()} behavior",
|
|
1381
1415
|
"",
|
|
1382
1416
|
"BORIS map files (*.boris_map);;All files (*)",
|
|
1383
1417
|
)
|
|
@@ -1759,24 +1793,40 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
1759
1793
|
return {cfg.CANCEL: True}
|
|
1760
1794
|
|
|
1761
1795
|
# check if behavior belong to category that is not in categories list
|
|
1762
|
-
|
|
1796
|
+
missing_behavior_category: list = []
|
|
1763
1797
|
for idx in checked_ethogram:
|
|
1764
1798
|
if cfg.BEHAVIOR_CATEGORY in checked_ethogram[idx]:
|
|
1765
1799
|
if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY]:
|
|
1766
1800
|
if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY] not in self.pj[cfg.BEHAVIORAL_CATEGORIES]:
|
|
1767
|
-
|
|
1768
|
-
|
|
1801
|
+
missing_behavior_category.append(
|
|
1802
|
+
(checked_ethogram[idx][cfg.BEHAVIOR_CODE], checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY])
|
|
1803
|
+
)
|
|
1804
|
+
if missing_behavior_category:
|
|
1769
1805
|
response = dialog.MessageDialog(
|
|
1770
1806
|
f"{cfg.programName} - Behavioral categories",
|
|
1771
1807
|
(
|
|
1772
|
-
"The behavioral
|
|
1773
|
-
f"{', '.join(set(['<b>' + x[1] + '</b>' + ' (used with <b>' + x[0] + '</b>)' for x in
|
|
1774
|
-
"are
|
|
1808
|
+
"The behavioral category/ies<br> "
|
|
1809
|
+
f"{', '.join(set(['<b>' + x[1] + '</b>' + ' (used with <b>' + x[0] + '</b>)<br>' for x in missing_behavior_category]))} "
|
|
1810
|
+
"are not defined in behavioral categories list.<br>"
|
|
1775
1811
|
),
|
|
1776
|
-
["Add behavioral category/ies",
|
|
1812
|
+
["Add behavioral category/ies", cfg.IGNORE, cfg.CANCEL],
|
|
1777
1813
|
)
|
|
1778
1814
|
if response == "Add behavioral category/ies":
|
|
1779
|
-
|
|
1815
|
+
if cfg.BEHAVIORAL_CATEGORIES_CONF not in self.pj:
|
|
1816
|
+
self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = {}
|
|
1817
|
+
for x1 in set(x[1] for x in missing_behavior_category):
|
|
1818
|
+
self.pj[cfg.BEHAVIORAL_CATEGORIES].append(x1)
|
|
1819
|
+
|
|
1820
|
+
if self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF]:
|
|
1821
|
+
index = str(max([int(k) for k in self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF]]) + 1)
|
|
1822
|
+
else:
|
|
1823
|
+
index = "0"
|
|
1824
|
+
|
|
1825
|
+
self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF][index] = {
|
|
1826
|
+
"name": x1,
|
|
1827
|
+
cfg.COLOR: "",
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1780
1830
|
if response == cfg.CANCEL:
|
|
1781
1831
|
return {cfg.CANCEL: True}
|
|
1782
1832
|
|
boris/project_functions.py
CHANGED
|
@@ -356,7 +356,7 @@ def check_state_events(pj: dict, observations_list: list) -> Tuple[bool, tuple]:
|
|
|
356
356
|
use check_state_events_obs function
|
|
357
357
|
"""
|
|
358
358
|
|
|
359
|
-
logging.info("Check state events")
|
|
359
|
+
logging.info("Check state events function")
|
|
360
360
|
|
|
361
361
|
out = ""
|
|
362
362
|
not_paired_obs_list = []
|
|
@@ -404,7 +404,7 @@ def check_project_integrity(
|
|
|
404
404
|
* check if media file are available (optional)
|
|
405
405
|
* check if media length available
|
|
406
406
|
* check independent variables
|
|
407
|
-
* check if coded subjects are
|
|
407
|
+
* check if coded subjects are defined
|
|
408
408
|
|
|
409
409
|
Args:
|
|
410
410
|
pj (dict): BORIS project
|
|
@@ -474,6 +474,7 @@ def check_project_integrity(
|
|
|
474
474
|
if not timestamp.is_nan() and not (-2147483647 <= timestamp <= 2147483647):
|
|
475
475
|
out_events += f"Observation: <b>{obs_id}</b><br>The timestamp {timestamp} is not between -2147483647 and 2147483647.<br>"
|
|
476
476
|
|
|
477
|
+
"""
|
|
477
478
|
# check if media length available
|
|
478
479
|
if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
|
|
479
480
|
for nplayer in cfg.ALL_PLAYERS:
|
|
@@ -484,6 +485,7 @@ def check_project_integrity(
|
|
|
484
485
|
except KeyError:
|
|
485
486
|
out += "<br><br>" if out else ""
|
|
486
487
|
out += f"Observation: <b>{obs_id}</b><br>Length not available for media file <b>{media_file}</b>"
|
|
488
|
+
"""
|
|
487
489
|
|
|
488
490
|
out += "<br><br>" if out else ""
|
|
489
491
|
out += out_events
|
|
@@ -530,25 +532,46 @@ def check_project_integrity(
|
|
|
530
532
|
]
|
|
531
533
|
)
|
|
532
534
|
|
|
533
|
-
|
|
535
|
+
tmp_out: str = ""
|
|
534
536
|
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
535
537
|
if cfg.INDEPENDENT_VARIABLES not in pj[cfg.OBSERVATIONS][obs_id]:
|
|
536
538
|
continue
|
|
537
539
|
for var_label in pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES]:
|
|
538
540
|
if var_label in defined_set_var_label:
|
|
539
541
|
if pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label] not in defined_set_var_label[var_label].split(","):
|
|
540
|
-
|
|
542
|
+
tmp_out += (
|
|
541
543
|
f"{obs_id}: the <b>{pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label]}</b> value "
|
|
542
544
|
f" is not allowed for {var_label} (choose between {defined_set_var_label[var_label]})<br>"
|
|
543
545
|
)
|
|
546
|
+
if tmp_out:
|
|
547
|
+
out += "<br><br>" if out else ""
|
|
548
|
+
out += tmp_out
|
|
544
549
|
|
|
545
550
|
# check if coded subjects are defined in the subjects list
|
|
551
|
+
tmp_out: str = ""
|
|
546
552
|
subjects_list: list = [pj[cfg.SUBJECTS][x]["name"] for x in pj[cfg.SUBJECTS]]
|
|
547
553
|
coded_subjects = set(util.flatten_list([[y[1] for y in pj[cfg.OBSERVATIONS][x].get(cfg.EVENTS, [])] for x in pj[cfg.OBSERVATIONS]]))
|
|
548
554
|
|
|
549
555
|
for subject in coded_subjects:
|
|
550
556
|
if subject and subject not in subjects_list:
|
|
551
|
-
|
|
557
|
+
tmp_out += f"The coded subject <b>{subject}</b> is not defined in the subjects list.<br>You can use the <b>Explore project</b> to fix it.<br><br>"
|
|
558
|
+
if tmp_out:
|
|
559
|
+
out += "<br><br>" if out else ""
|
|
560
|
+
out += tmp_out
|
|
561
|
+
|
|
562
|
+
# check if media file have info in media_info section of project
|
|
563
|
+
tmp_out: str = ""
|
|
564
|
+
for obs_id in pj[cfg.OBSERVATIONS]:
|
|
565
|
+
for player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
|
|
566
|
+
for media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][player]:
|
|
567
|
+
for info in (cfg.LENGTH, cfg.FPS, cfg.HAS_AUDIO, cfg.HAS_VIDEO):
|
|
568
|
+
if media_file not in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info]:
|
|
569
|
+
tmp_out += f"Observation <b>{obs_id}</b>:<br>"
|
|
570
|
+
tmp_out += f"The media file {media_file} has no <b>{info}</b> info.<br>"
|
|
571
|
+
if tmp_out:
|
|
572
|
+
tmp_out += "<br>You should repick the media file to fix this issue."
|
|
573
|
+
out += "<br><br>" if out else ""
|
|
574
|
+
out += tmp_out
|
|
552
575
|
|
|
553
576
|
return out
|
|
554
577
|
|
|
@@ -1168,13 +1191,8 @@ def observed_interval(observation: dict) -> Tuple[dec, dec]:
|
|
|
1168
1191
|
"""
|
|
1169
1192
|
if not observation[cfg.EVENTS]:
|
|
1170
1193
|
return (dec("0.0"), dec("0.0"))
|
|
1171
|
-
if observation[cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
|
|
1172
|
-
"""
|
|
1173
|
-
print("=" * 120)
|
|
1174
|
-
print(observation[cfg.EVENTS])
|
|
1175
|
-
print("=" * 120)
|
|
1176
|
-
"""
|
|
1177
1194
|
|
|
1195
|
+
if observation[cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
|
|
1178
1196
|
event_timestamp = [event[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.TIME]] for event in observation[cfg.EVENTS]]
|
|
1179
1197
|
|
|
1180
1198
|
return (
|
|
@@ -1183,8 +1201,12 @@ def observed_interval(observation: dict) -> Tuple[dec, dec]:
|
|
|
1183
1201
|
)
|
|
1184
1202
|
if observation[cfg.TYPE] == cfg.IMAGES:
|
|
1185
1203
|
events = [x[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.IMAGE_INDEX]] for x in observation[cfg.EVENTS]]
|
|
1186
|
-
|
|
1187
|
-
|
|
1204
|
+
# test if indexes contain NA
|
|
1205
|
+
try:
|
|
1206
|
+
dec(min(events))
|
|
1207
|
+
return (dec(min(events)), dec(max(events)))
|
|
1208
|
+
except Exception:
|
|
1209
|
+
return (dec("NaN"), dec("NaN"))
|
|
1188
1210
|
|
|
1189
1211
|
|
|
1190
1212
|
def events_start_stop(ethogram: dict, events: list, obs_type: str) -> List[tuple]:
|
|
@@ -1273,7 +1295,7 @@ def open_project_json(project_file_name: str) -> tuple:
|
|
|
1273
1295
|
str: message
|
|
1274
1296
|
"""
|
|
1275
1297
|
|
|
1276
|
-
logging.debug(f"
|
|
1298
|
+
logging.debug(f"open_project_json function: {project_file_name}")
|
|
1277
1299
|
|
|
1278
1300
|
projectChanged: bool = False
|
|
1279
1301
|
msg: str = ""
|
|
@@ -1612,6 +1634,8 @@ def fix_unpaired_state_events2(ethogram: dict, events: list, fix_at_time: dec) -
|
|
|
1612
1634
|
list: list of events with state events fixed
|
|
1613
1635
|
"""
|
|
1614
1636
|
|
|
1637
|
+
logging.debug("fix_unpaired_state_events2 function")
|
|
1638
|
+
|
|
1615
1639
|
closing_events_to_add: list = []
|
|
1616
1640
|
subjects: list = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in events]
|
|
1617
1641
|
ethogram_behaviors: dict = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}
|