boris-behav-obs 9.3.3__py2.py3-none-any.whl → 9.3.5__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 +2 -0
- boris/core.py +21 -103
- boris/core_qrc.py +6580 -6784
- boris/dialog.py +78 -9
- boris/edit_event.py +47 -27
- boris/edit_event_ui.py +69 -36
- boris/event_operations.py +52 -47
- boris/observation.py +1 -2
- boris/observation_operations.py +34 -12
- boris/project.py +11 -9
- boris/project_functions.py +39 -15
- boris/select_observations.py +7 -1
- boris/state_events.py +1 -1
- boris/utilities.py +1 -1
- boris/version.py +2 -2
- {boris_behav_obs-9.3.3.dist-info → boris_behav_obs-9.3.5.dist-info}/METADATA +2 -2
- {boris_behav_obs-9.3.3.dist-info → boris_behav_obs-9.3.5.dist-info}/RECORD +21 -21
- {boris_behav_obs-9.3.3.dist-info → boris_behav_obs-9.3.5.dist-info}/WHEEL +1 -1
- {boris_behav_obs-9.3.3.dist-info → boris_behav_obs-9.3.5.dist-info}/entry_points.txt +0 -0
- {boris_behav_obs-9.3.3.dist-info → boris_behav_obs-9.3.5.dist-info}/licenses/LICENSE.TXT +0 -0
- {boris_behav_obs-9.3.3.dist-info → boris_behav_obs-9.3.5.dist-info}/top_level.txt +0 -0
boris/event_operations.py
CHANGED
|
@@ -70,7 +70,7 @@ def add_event(self):
|
|
|
70
70
|
|
|
71
71
|
editWindow = DlgEditEvent(
|
|
72
72
|
observation_type=self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
|
|
73
|
-
time_value=
|
|
73
|
+
time_value=dec("NaN"),
|
|
74
74
|
image_idx=0,
|
|
75
75
|
current_time=current_time,
|
|
76
76
|
time_format=self.timeFormat,
|
|
@@ -80,8 +80,6 @@ def add_event(self):
|
|
|
80
80
|
|
|
81
81
|
sortedSubjects = [cfg.NO_FOCAL_SUBJECT] + sorted([self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]])
|
|
82
82
|
|
|
83
|
-
print(f"{sortedSubjects=}")
|
|
84
|
-
|
|
85
83
|
editWindow.cobSubject.addItems(sortedSubjects)
|
|
86
84
|
if self.currentSubject:
|
|
87
85
|
editWindow.cobSubject.setCurrentIndex(editWindow.cobSubject.findText(self.currentSubject, Qt.MatchFixedString))
|
|
@@ -173,8 +171,13 @@ def add_event(self):
|
|
|
173
171
|
time_ = dec("NaN")
|
|
174
172
|
if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False):
|
|
175
173
|
if self.playerType != cfg.VIEWER_IMAGES:
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
exif_date_time = util.extract_exif_DateTimeOriginal(self.images_list[new_index])
|
|
175
|
+
if exif_date_time != -1:
|
|
176
|
+
time_ = exif_date_time
|
|
177
|
+
|
|
178
|
+
# check if first value must be substracted
|
|
179
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.SUBSTRACT_FIRST_EXIF_DATE, True):
|
|
180
|
+
time_ -= self.image_time_ref
|
|
178
181
|
|
|
179
182
|
elif self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0):
|
|
180
183
|
time_ = new_index * self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0)
|
|
@@ -591,6 +594,12 @@ def edit_event(self):
|
|
|
591
594
|
else:
|
|
592
595
|
current_value = self.getLaps()
|
|
593
596
|
|
|
597
|
+
# get exif date time
|
|
598
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False):
|
|
599
|
+
exif_date_time = util.extract_exif_DateTimeOriginal(self.images_list[self.image_idx])
|
|
600
|
+
else:
|
|
601
|
+
exif_date_time = None
|
|
602
|
+
|
|
594
603
|
edit_window = DlgEditEvent(
|
|
595
604
|
observation_type=self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
|
|
596
605
|
time_value=time_value,
|
|
@@ -598,12 +607,15 @@ def edit_event(self):
|
|
|
598
607
|
current_time=current_value,
|
|
599
608
|
time_format=self.timeFormat,
|
|
600
609
|
show_set_current_time=True,
|
|
610
|
+
exif_date_time=exif_date_time,
|
|
601
611
|
)
|
|
602
612
|
edit_window.setWindowTitle("Edit event")
|
|
603
613
|
|
|
604
614
|
# time
|
|
605
615
|
if time_value.is_nan():
|
|
606
616
|
edit_window.cb_set_time_na.setChecked(True)
|
|
617
|
+
|
|
618
|
+
# remove visibility of 'set current time' widget if VIEWER mode
|
|
607
619
|
if self.playerType in (cfg.VIEWER_MEDIA, cfg.VIEWER_LIVE, cfg.VIEWER_IMAGES):
|
|
608
620
|
edit_window.pb_set_to_current_time.setVisible(False)
|
|
609
621
|
|
|
@@ -637,20 +649,16 @@ def edit_event(self):
|
|
|
637
649
|
sortedCodes.index(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX])
|
|
638
650
|
)
|
|
639
651
|
else:
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
"does not exist longer in the ethogram"
|
|
644
|
-
)
|
|
652
|
+
msg: str = (
|
|
653
|
+
f"The behaviour {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX]} "
|
|
654
|
+
"does not exist longer in the ethogram"
|
|
645
655
|
)
|
|
656
|
+
logging.warning(msg)
|
|
657
|
+
|
|
646
658
|
QMessageBox.warning(
|
|
647
659
|
self,
|
|
648
660
|
cfg.programName,
|
|
649
|
-
|
|
650
|
-
"The behaviour "
|
|
651
|
-
f"<b>{self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX]}</b> "
|
|
652
|
-
"does not exist more in the ethogram"
|
|
653
|
-
),
|
|
661
|
+
msg,
|
|
654
662
|
)
|
|
655
663
|
edit_window.cobCode.setCurrentIndex(0)
|
|
656
664
|
|
|
@@ -658,28 +666,32 @@ def edit_event(self):
|
|
|
658
666
|
f"original modifiers: {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_MODIFIER_FIELD_IDX]}"
|
|
659
667
|
)
|
|
660
668
|
|
|
661
|
-
# frame index
|
|
662
|
-
frame_idx = read_event_field(
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
)
|
|
667
|
-
edit_window.sb_frame_idx.setValue(0 if frame_idx in (cfg.NA, None) else frame_idx)
|
|
668
|
-
if frame_idx in (cfg.NA, None):
|
|
669
|
-
|
|
669
|
+
# # frame index
|
|
670
|
+
# frame_idx = read_event_field(
|
|
671
|
+
# self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx],
|
|
672
|
+
# self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
|
|
673
|
+
# cfg.FRAME_INDEX,
|
|
674
|
+
# )
|
|
675
|
+
# edit_window.sb_frame_idx.setValue(0 if frame_idx in (cfg.NA, None) else frame_idx)
|
|
676
|
+
# if frame_idx in (cfg.NA, None):
|
|
677
|
+
# edit_window.cb_set_frame_idx_na.setChecked(True)
|
|
670
678
|
|
|
671
679
|
# comment
|
|
672
680
|
edit_window.leComment.setPlainText(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_COMMENT_FIELD_IDX])
|
|
673
681
|
|
|
674
682
|
flag_ok = False # for looping until event is OK or Cancel pressed
|
|
675
683
|
while True:
|
|
676
|
-
if edit_window.
|
|
684
|
+
if edit_window.exec(): # button OK
|
|
677
685
|
self.project_changed()
|
|
678
686
|
|
|
687
|
+
new_time = edit_window.time_widget.get_time()
|
|
688
|
+
|
|
689
|
+
if edit_window.cb_set_time_na.isChecked():
|
|
690
|
+
new_time = dec("NaN")
|
|
691
|
+
|
|
679
692
|
# MEDIA / LIVE
|
|
680
693
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
|
|
681
|
-
new_time
|
|
682
|
-
if new_time is None:
|
|
694
|
+
if new_time.is_nan():
|
|
683
695
|
QMessageBox.warning(
|
|
684
696
|
self,
|
|
685
697
|
cfg.programName,
|
|
@@ -707,19 +719,17 @@ def edit_event(self):
|
|
|
707
719
|
event[cfg.FRAME_INDEX] = frame_idx
|
|
708
720
|
self.seek_mediaplayer(mem_time)
|
|
709
721
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
# event[cfg.FRAME_INDEX] = edit_window.sb_frame_idx.value()
|
|
722
|
-
"""
|
|
722
|
+
# if not edit_window.sb_frame_idx.value() or edit_window.cb_set_frame_idx_na.isChecked():
|
|
723
|
+
# event[cfg.FRAME_INDEX] = cfg.NA
|
|
724
|
+
# else:
|
|
725
|
+
# if self.playerType == cfg.MEDIA:
|
|
726
|
+
# mem_time = self.getLaps()
|
|
727
|
+
# if not self.seek_mediaplayer(new_time):
|
|
728
|
+
# frame_idx = self.get_frame_index()
|
|
729
|
+
# event[cfg.FRAME_INDEX] = frame_idx
|
|
730
|
+
# self.seek_mediaplayer(mem_time)
|
|
731
|
+
#
|
|
732
|
+
# # event[cfg.FRAME_INDEX] = edit_window.sb_frame_idx.value()
|
|
723
733
|
|
|
724
734
|
event["row"] = pj_event_idx
|
|
725
735
|
event["original_modifiers"] = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
|
|
@@ -747,10 +757,7 @@ def edit_event(self):
|
|
|
747
757
|
for key in self.pj[cfg.ETHOGRAM]:
|
|
748
758
|
if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
|
|
749
759
|
event = self.full_event(key)
|
|
750
|
-
|
|
751
|
-
event[cfg.TIME] = dec("NaN")
|
|
752
|
-
else:
|
|
753
|
-
event[cfg.TIME] = edit_window.time_widget.get_time()
|
|
760
|
+
event[cfg.TIME] = new_time
|
|
754
761
|
|
|
755
762
|
event[cfg.SUBJECT] = (
|
|
756
763
|
"" if edit_window.cobSubject.currentText() == cfg.NO_FOCAL_SUBJECT else edit_window.cobSubject.currentText()
|
|
@@ -808,7 +815,7 @@ def edit_time_selected_events(self):
|
|
|
808
815
|
return
|
|
809
816
|
|
|
810
817
|
d = w.time_widget.get_time()
|
|
811
|
-
if d
|
|
818
|
+
if d.is_nan() or not d:
|
|
812
819
|
return
|
|
813
820
|
|
|
814
821
|
if ":" in util.smart_time_format(abs(d)):
|
|
@@ -882,8 +889,6 @@ def copy_selected_events(self):
|
|
|
882
889
|
else:
|
|
883
890
|
copied_events.append("\t".join([str(x) for x in event]))
|
|
884
891
|
|
|
885
|
-
print(f"{copied_events=}")
|
|
886
|
-
|
|
887
892
|
cb = QApplication.clipboard()
|
|
888
893
|
cb.clear(mode=QClipboard.Mode.Clipboard)
|
|
889
894
|
cb.setText("\n".join(copied_events), mode=QClipboard.Mode.Clipboard)
|
boris/observation.py
CHANGED
|
@@ -24,10 +24,9 @@ import logging
|
|
|
24
24
|
import os
|
|
25
25
|
import pandas as pd
|
|
26
26
|
import pathlib as pl
|
|
27
|
-
import datetime as dt
|
|
28
27
|
|
|
29
28
|
from PySide6.QtCore import Qt
|
|
30
|
-
from PySide6.QtGui import QColor
|
|
29
|
+
from PySide6.QtGui import QColor
|
|
31
30
|
from PySide6.QtWidgets import (
|
|
32
31
|
QDialog,
|
|
33
32
|
QVBoxLayout,
|
boris/observation_operations.py
CHANGED
|
@@ -1024,7 +1024,21 @@ def new_observation(self, mode: str = cfg.NEW, obsId: str = "") -> None:
|
|
|
1024
1024
|
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.DIRECTORIES_LIST] = [
|
|
1025
1025
|
observationWindow.lw_images_directory.item(i).text() for i in range(observationWindow.lw_images_directory.count())
|
|
1026
1026
|
]
|
|
1027
|
+
|
|
1028
|
+
# check if exif data must be used
|
|
1027
1029
|
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.USE_EXIF_DATE] = observationWindow.rb_use_exif.isChecked()
|
|
1030
|
+
|
|
1031
|
+
# ask if the value of the exif date time of the first picture must be substracted
|
|
1032
|
+
# TODO: improve this
|
|
1033
|
+
if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.USE_EXIF_DATE]:
|
|
1034
|
+
response = dialog.MessageDialog(
|
|
1035
|
+
cfg.programName,
|
|
1036
|
+
"You choose to use the EXIF metadata. Do you want to substract the date time value of the first picture?",
|
|
1037
|
+
(cfg.YES, cfg.NO),
|
|
1038
|
+
)
|
|
1039
|
+
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.SUBSTRACT_FIRST_EXIF_DATE] = response == cfg.YES
|
|
1040
|
+
|
|
1041
|
+
# check if time lapse
|
|
1028
1042
|
if observationWindow.rb_time_lapse.isChecked():
|
|
1029
1043
|
self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TIME_LAPSE] = observationWindow.sb_time_lapse.value()
|
|
1030
1044
|
else:
|
|
@@ -1086,7 +1100,6 @@ def new_observation(self, mode: str = cfg.NEW, obsId: str = "") -> None:
|
|
|
1086
1100
|
return "Observation not launched"
|
|
1087
1101
|
|
|
1088
1102
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
1089
|
-
# QMessageBox.critical(self, cfg.programName, "Observation from images directory is not yet implemented")
|
|
1090
1103
|
initialize_new_images_observation(self)
|
|
1091
1104
|
|
|
1092
1105
|
self.load_tw_events(self.observationId)
|
|
@@ -1100,7 +1113,7 @@ def close_observation(self):
|
|
|
1100
1113
|
|
|
1101
1114
|
logging.info(f"Close observation (player type: {self.playerType})")
|
|
1102
1115
|
|
|
1103
|
-
# check observation events
|
|
1116
|
+
# check observation state events
|
|
1104
1117
|
|
|
1105
1118
|
flag_ok, msg = project_functions.check_state_events_obs(
|
|
1106
1119
|
self.observationId,
|
|
@@ -1111,16 +1124,22 @@ def close_observation(self):
|
|
|
1111
1124
|
|
|
1112
1125
|
if not flag_ok:
|
|
1113
1126
|
out = f"The current observation has state event(s) that are not PAIRED:<br><br>{msg}"
|
|
1114
|
-
results = dialog.
|
|
1127
|
+
results = dialog.Results_dialog_exit_code()
|
|
1115
1128
|
results.setWindowTitle(f"{cfg.programName} - Check selected observations")
|
|
1116
1129
|
results.ptText.setReadOnly(True)
|
|
1117
1130
|
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
1131
|
|
|
1123
|
-
|
|
1132
|
+
results.pb1.setText("Close observation")
|
|
1133
|
+
results.pb2.setText("Return to observation")
|
|
1134
|
+
if self.playerType == cfg.IMAGES:
|
|
1135
|
+
results.pb3.setVisible(False)
|
|
1136
|
+
else:
|
|
1137
|
+
results.pb3.setText("Fix unpaired state events")
|
|
1138
|
+
|
|
1139
|
+
r = results.exec()
|
|
1140
|
+
if r == 2: # Return to observation
|
|
1141
|
+
return
|
|
1142
|
+
if r == 3: # Fix unpaired state events
|
|
1124
1143
|
state_events.fix_unpaired_events(self, silent_mode=True)
|
|
1125
1144
|
|
|
1126
1145
|
self.saved_state = self.saveState()
|
|
@@ -1280,7 +1299,7 @@ def check_creation_date(self) -> Tuple[int, dict]:
|
|
|
1280
1299
|
def init_mpv(self):
|
|
1281
1300
|
"""Start mpv process and embed it in the PySide6 application."""
|
|
1282
1301
|
|
|
1283
|
-
|
|
1302
|
+
logging.debug("function: init_mpv")
|
|
1284
1303
|
|
|
1285
1304
|
"""
|
|
1286
1305
|
print(f"{self.winId()=}")
|
|
@@ -1307,7 +1326,7 @@ def init_mpv(self):
|
|
|
1307
1326
|
def send_command(command):
|
|
1308
1327
|
"""Send a JSON command to the mpv IPC server."""
|
|
1309
1328
|
|
|
1310
|
-
|
|
1329
|
+
logging.debug("function: send commnand {command}")
|
|
1311
1330
|
# print(f"{self.mpv_process=}")
|
|
1312
1331
|
|
|
1313
1332
|
try:
|
|
@@ -2407,8 +2426,11 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
|
|
|
2407
2426
|
|
|
2408
2427
|
cumul_media_durations: list = [dec(0)]
|
|
2409
2428
|
for media_file in observation[cfg.FILE][cfg.PLAYER1]:
|
|
2410
|
-
|
|
2411
|
-
|
|
2429
|
+
try:
|
|
2430
|
+
media_duration = dec(str(observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]))
|
|
2431
|
+
cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
|
|
2432
|
+
except KeyError:
|
|
2433
|
+
return None
|
|
2412
2434
|
|
|
2413
2435
|
cumul_media_durations.remove(dec(0))
|
|
2414
2436
|
|
boris/project.py
CHANGED
|
@@ -150,12 +150,14 @@ class BehavioralCategories(QDialog):
|
|
|
150
150
|
sorted([pj[cfg.ETHOGRAM][idx].get(cfg.BEHAVIOR_CATEGORY, "") for idx in pj.get(cfg.ETHOGRAM, {})])
|
|
151
151
|
)
|
|
152
152
|
|
|
153
|
-
if behavioral_categories_in_ethogram.difference(set(behavioral_categories))
|
|
153
|
+
if behavioral_categories_in_ethogram.difference(set(behavioral_categories)) and behavioral_categories_in_ethogram.difference(
|
|
154
|
+
set(behavioral_categories)
|
|
155
|
+
) != {""}:
|
|
154
156
|
if (
|
|
155
157
|
dialog.MessageDialog(
|
|
156
158
|
cfg.programName,
|
|
157
159
|
(
|
|
158
|
-
"
|
|
160
|
+
"There are behavioral categories that are present in ethogram but not defined.<br>"
|
|
159
161
|
f"{behavioral_categories_in_ethogram.difference(set(behavioral_categories))}<br>"
|
|
160
162
|
"<br>"
|
|
161
163
|
"Do you want to add them in the behavioral categories list?"
|
|
@@ -210,7 +212,6 @@ class BehavioralCategories(QDialog):
|
|
|
210
212
|
color = col_diag.currentColor()
|
|
211
213
|
if color.name() == "#000000": # black -> delete color
|
|
212
214
|
self.lw.item(row, 1).setText("")
|
|
213
|
-
# self.lw.item(row, 1).setBackground(QColor(230, 230, 230))
|
|
214
215
|
self.lw.item(row, 1).setBackground(self.not_editable_column_color())
|
|
215
216
|
elif color.isValid():
|
|
216
217
|
self.lw.item(row, 1).setText(color.name())
|
|
@@ -820,18 +821,18 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
820
821
|
column (int): column double-clicked
|
|
821
822
|
"""
|
|
822
823
|
|
|
823
|
-
#
|
|
824
|
+
# excluded column
|
|
824
825
|
if column == cfg.behavioursFields[cfg.EXCLUDED]:
|
|
825
826
|
self.exclusion_matrix()
|
|
826
827
|
|
|
827
|
-
#
|
|
828
|
+
# coding map
|
|
828
829
|
if column == cfg.behavioursFields[cfg.CODING_MAP_sp]:
|
|
829
830
|
if "with coding map" in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
|
|
830
831
|
self.behavior_type_changed(row)
|
|
831
832
|
else:
|
|
832
833
|
QMessageBox.information(self, cfg.programName, "Change the behavior type on first column to select a coding map")
|
|
833
834
|
|
|
834
|
-
#
|
|
835
|
+
# behavior type
|
|
835
836
|
if column == cfg.behavioursFields["type"]:
|
|
836
837
|
self.behavior_type_doubleclicked(row)
|
|
837
838
|
|
|
@@ -843,7 +844,7 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
843
844
|
if column == cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]:
|
|
844
845
|
self.category_doubleclicked(row)
|
|
845
846
|
|
|
846
|
-
#
|
|
847
|
+
# modifiers
|
|
847
848
|
if column == cfg.behavioursFields[cfg.MODIFIERS]:
|
|
848
849
|
# check if behavior has coding map
|
|
849
850
|
if (
|
|
@@ -897,16 +898,17 @@ class projectDialog(QDialog, Ui_dlgProject):
|
|
|
897
898
|
if self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).text():
|
|
898
899
|
current_color = QColor(self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).text())
|
|
899
900
|
if current_color.isValid():
|
|
901
|
+
print(f"{current_color=}")
|
|
900
902
|
col_diag.setCurrentColor(current_color)
|
|
901
903
|
|
|
902
|
-
if col_diag.
|
|
904
|
+
if col_diag.exec():
|
|
903
905
|
color = col_diag.currentColor()
|
|
904
906
|
if color.name() == "#000000": # black -> delete color
|
|
905
907
|
self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText("")
|
|
906
908
|
self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(self.not_editable_column_color())
|
|
907
909
|
elif color.isValid():
|
|
910
|
+
self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(QColor(color.name()))
|
|
908
911
|
self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText(color.name())
|
|
909
|
-
self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(color)
|
|
910
912
|
|
|
911
913
|
def category_doubleclicked(self, row):
|
|
912
914
|
"""
|
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}
|
|
@@ -1960,6 +1984,6 @@ def project2dataframe(pj: dict, observations_list: list = []) -> pd.DataFrame:
|
|
|
1960
1984
|
|
|
1961
1985
|
pd.DataFrame(data).info()
|
|
1962
1986
|
|
|
1963
|
-
print(pd.DataFrame(data))
|
|
1987
|
+
# print(pd.DataFrame(data))
|
|
1964
1988
|
|
|
1965
1989
|
return pd.DataFrame(data)
|
boris/select_observations.py
CHANGED
|
@@ -21,7 +21,9 @@ Copyright 2012-2025 Olivier Friard
|
|
|
21
21
|
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
import logging
|
|
24
25
|
from typing import Tuple
|
|
26
|
+
|
|
25
27
|
from PySide6.QtCore import Qt
|
|
26
28
|
from PySide6.QtWidgets import QAbstractItemView
|
|
27
29
|
|
|
@@ -60,7 +62,10 @@ def select_observations2(self, mode: str, windows_title: str = "") -> Tuple[str,
|
|
|
60
62
|
data: list = []
|
|
61
63
|
not_paired: list = []
|
|
62
64
|
|
|
65
|
+
# check if observations changed
|
|
63
66
|
if hash(str(self.pj[cfg.OBSERVATIONS])) != self.mem_hash_obs:
|
|
67
|
+
logging.debug("observations changed")
|
|
68
|
+
|
|
64
69
|
for obs in sorted(list(pj[cfg.OBSERVATIONS].keys())):
|
|
65
70
|
date = pj[cfg.OBSERVATIONS][obs]["date"].replace("T", " ")
|
|
66
71
|
descr = util.eol2space(pj[cfg.OBSERVATIONS][obs][cfg.DESCRIPTION])
|
|
@@ -124,11 +129,12 @@ def select_observations2(self, mode: str, windows_title: str = "") -> Tuple[str,
|
|
|
124
129
|
data, header=fields_list + indep_var_header, column_type=column_type, not_paired=not_paired
|
|
125
130
|
)
|
|
126
131
|
self.data = data
|
|
132
|
+
self.not_paired = not_paired
|
|
127
133
|
self.mem_hash_obs = hash(str(self.pj[cfg.OBSERVATIONS]))
|
|
128
134
|
|
|
129
135
|
else:
|
|
130
136
|
obsList = observations_list.observationsList_widget(
|
|
131
|
-
self.data, header=fields_list + indep_var_header, column_type=column_type, not_paired=not_paired
|
|
137
|
+
self.data, header=fields_list + indep_var_header, column_type=column_type, not_paired=self.not_paired
|
|
132
138
|
)
|
|
133
139
|
|
|
134
140
|
if windows_title:
|
boris/state_events.py
CHANGED
boris/utilities.py
CHANGED
|
@@ -129,7 +129,7 @@ def extract_video_creation_date(file_path: str) -> int | None:
|
|
|
129
129
|
return None
|
|
130
130
|
|
|
131
131
|
|
|
132
|
-
def extract_date_time_from_file_name(file_path: str) -> int:
|
|
132
|
+
def extract_date_time_from_file_name(file_path: str) -> int | None:
|
|
133
133
|
"""
|
|
134
134
|
extract YYYY-MM-DD_HHMMSS or YYYY-MM-DD_HH:MM:SS from file name
|
|
135
135
|
"""
|
boris/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boris-behav-obs
|
|
3
|
-
Version: 9.3.
|
|
3
|
+
Version: 9.3.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
|
|
@@ -24,7 +24,7 @@ Requires-Dist: matplotlib>=3.3.3
|
|
|
24
24
|
Requires-Dist: pandas>=2.2.2
|
|
25
25
|
Requires-Dist: tablib[cli,html,ods,pandas,xls,xlsx]>=3
|
|
26
26
|
Requires-Dist: pyreadr
|
|
27
|
-
Requires-Dist: pyside6==6.
|
|
27
|
+
Requires-Dist: pyside6==6.9
|
|
28
28
|
Requires-Dist: hachoir>=3.3.0
|
|
29
29
|
Provides-Extra: dev
|
|
30
30
|
Requires-Dist: ruff; extra == "dev"
|