boris-behav-obs 9.7.7__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.
Potentially problematic release.
This version of boris-behav-obs might be problematic. Click here for more details.
- boris/__init__.py +26 -0
- boris/__main__.py +25 -0
- boris/about.py +143 -0
- boris/add_modifier.py +635 -0
- boris/add_modifier_ui.py +303 -0
- boris/advanced_event_filtering.py +455 -0
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +1110 -0
- boris/behavior_binary_table.py +305 -0
- boris/behaviors_coding_map.py +239 -0
- boris/boris_cli.py +340 -0
- boris/cmd_arguments.py +49 -0
- boris/coding_pad.py +280 -0
- boris/config.py +785 -0
- boris/config_file.py +356 -0
- boris/connections.py +409 -0
- boris/converters.py +333 -0
- boris/converters_ui.py +225 -0
- boris/cooccurence.py +250 -0
- boris/core.py +5901 -0
- boris/core_qrc.py +15958 -0
- boris/core_ui.py +1107 -0
- boris/db_functions.py +324 -0
- boris/dev.py +134 -0
- boris/dialog.py +1108 -0
- boris/duration_widget.py +238 -0
- boris/edit_event.py +245 -0
- boris/edit_event_ui.py +233 -0
- boris/event_operations.py +1040 -0
- boris/events_cursor.py +61 -0
- boris/events_snapshots.py +596 -0
- boris/exclusion_matrix.py +141 -0
- boris/export_events.py +1006 -0
- boris/export_observation.py +1203 -0
- boris/external_processes.py +332 -0
- boris/geometric_measurement.py +941 -0
- boris/gui_utilities.py +135 -0
- boris/image_overlay.py +72 -0
- boris/import_observations.py +242 -0
- boris/ipc_mpv.py +325 -0
- boris/irr.py +634 -0
- boris/latency.py +244 -0
- boris/measurement_widget.py +161 -0
- boris/media_file.py +115 -0
- boris/menu_options.py +213 -0
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +157 -0
- boris/mpv.py +2016 -0
- boris/mpv2.py +2193 -0
- boris/observation.py +1453 -0
- boris/observation_operations.py +2538 -0
- boris/observation_ui.py +679 -0
- boris/observations_list.py +337 -0
- boris/otx_parser.py +442 -0
- boris/param_panel.py +201 -0
- boris/param_panel_ui.py +305 -0
- boris/player_dock_widget.py +198 -0
- boris/plot_data_module.py +536 -0
- boris/plot_events.py +634 -0
- boris/plot_events_rt.py +237 -0
- boris/plot_spectrogram_rt.py +316 -0
- boris/plot_waveform_rt.py +230 -0
- boris/plugins.py +431 -0
- boris/portion/__init__.py +31 -0
- boris/portion/const.py +95 -0
- boris/portion/dict.py +365 -0
- boris/portion/func.py +52 -0
- boris/portion/interval.py +581 -0
- boris/portion/io.py +181 -0
- boris/preferences.py +510 -0
- boris/preferences_ui.py +770 -0
- boris/project.py +2007 -0
- boris/project_functions.py +2041 -0
- boris/project_import_export.py +1096 -0
- boris/project_ui.py +794 -0
- boris/qrc_boris.py +10389 -0
- boris/qrc_boris5.py +2579 -0
- boris/select_modifiers.py +312 -0
- boris/select_observations.py +210 -0
- boris/select_subj_behav.py +286 -0
- boris/state_events.py +197 -0
- boris/subjects_pad.py +106 -0
- boris/synthetic_time_budget.py +290 -0
- boris/time_budget_functions.py +1136 -0
- boris/time_budget_widget.py +1039 -0
- boris/transitions.py +365 -0
- boris/utilities.py +1810 -0
- boris/version.py +24 -0
- boris/video_equalizer.py +159 -0
- boris/video_equalizer_ui.py +248 -0
- boris/video_operations.py +310 -0
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
- boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
- boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
- boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
- boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This file is part of BORIS.
|
|
7
|
+
|
|
8
|
+
BORIS is free software; you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation; either version 3 of the License, or
|
|
11
|
+
any later version.
|
|
12
|
+
|
|
13
|
+
BORIS is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program; if not see <http://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import binascii
|
|
24
|
+
import io
|
|
25
|
+
import json
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
import gui_utilities
|
|
28
|
+
|
|
29
|
+
from PySide6.QtCore import QBuffer, QByteArray, QIODevice, QLineF, QPoint, Qt, Signal
|
|
30
|
+
from PySide6.QtGui import QBrush, QColor, QIcon, QMouseEvent, QPen, QPixmap, QPolygonF, QAction
|
|
31
|
+
from PySide6.QtWidgets import (
|
|
32
|
+
QApplication,
|
|
33
|
+
QColorDialog,
|
|
34
|
+
QFileDialog,
|
|
35
|
+
QFrame,
|
|
36
|
+
QGraphicsEllipseItem,
|
|
37
|
+
QGraphicsLineItem,
|
|
38
|
+
QGraphicsPixmapItem,
|
|
39
|
+
QGraphicsPolygonItem,
|
|
40
|
+
QGraphicsScene,
|
|
41
|
+
QGraphicsView,
|
|
42
|
+
QHBoxLayout,
|
|
43
|
+
QInputDialog,
|
|
44
|
+
QLabel,
|
|
45
|
+
QLineEdit,
|
|
46
|
+
QListWidget,
|
|
47
|
+
QMainWindow,
|
|
48
|
+
QMessageBox,
|
|
49
|
+
QPushButton,
|
|
50
|
+
QSizePolicy,
|
|
51
|
+
QSlider,
|
|
52
|
+
QSpacerItem,
|
|
53
|
+
QSplitter,
|
|
54
|
+
QVBoxLayout,
|
|
55
|
+
QWidget,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
from . import config as cfg
|
|
59
|
+
from . import dialog
|
|
60
|
+
from . import utilities as util
|
|
61
|
+
|
|
62
|
+
designColor = QColor(255, 0, 0, 128) # red opacity: 50%
|
|
63
|
+
penWidth = 0
|
|
64
|
+
penStyle = Qt.NoPen
|
|
65
|
+
selectedBrush = QBrush()
|
|
66
|
+
selectedBrush.setStyle(Qt.SolidPattern)
|
|
67
|
+
selectedBrush.setColor(QColor(255, 255, 0, 255))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BehaviorsMapCreatorWindow(QMainWindow):
|
|
71
|
+
signal_add_to_project = Signal(dict)
|
|
72
|
+
|
|
73
|
+
class View(QGraphicsView):
|
|
74
|
+
"""
|
|
75
|
+
class for handling mousepress event in QGraphicsView
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
mousePress = Signal(QMouseEvent)
|
|
79
|
+
|
|
80
|
+
def mousePressEvent(self, event):
|
|
81
|
+
self.mousePress.emit(event)
|
|
82
|
+
|
|
83
|
+
_start = 0
|
|
84
|
+
elList, points = [], []
|
|
85
|
+
|
|
86
|
+
def __init__(self, parent):
|
|
87
|
+
QGraphicsView.__init__(self, parent)
|
|
88
|
+
self.setBackgroundBrush(QColor(128, 128, 128))
|
|
89
|
+
self.setScene(QGraphicsScene(self))
|
|
90
|
+
self.scene().update()
|
|
91
|
+
|
|
92
|
+
bitmapFileName, mapName, fileName = "", "", ""
|
|
93
|
+
flagNewArea, flag_map_changed = False, False
|
|
94
|
+
polygonsList2 = []
|
|
95
|
+
areaColor = QColor("lime")
|
|
96
|
+
|
|
97
|
+
def __init__(self, arg):
|
|
98
|
+
self.codes_list = arg
|
|
99
|
+
|
|
100
|
+
super(BehaviorsMapCreatorWindow, self).__init__()
|
|
101
|
+
|
|
102
|
+
self.pixmap = QPixmap()
|
|
103
|
+
self.closedPolygon = None
|
|
104
|
+
self.selectedPolygon = None
|
|
105
|
+
|
|
106
|
+
self.setWindowTitle("BORIS - Behaviors coding map creator")
|
|
107
|
+
|
|
108
|
+
self.newMapAction = QAction(QIcon(), "&New behaviors coding map", self)
|
|
109
|
+
self.newMapAction.setShortcut("Ctrl+N")
|
|
110
|
+
self.newMapAction.setStatusTip("Create a new behaviors coding map")
|
|
111
|
+
self.newMapAction.triggered.connect(self.newMap)
|
|
112
|
+
|
|
113
|
+
self.openMapAction = QAction(QIcon(), "&Open a behaviors coding map", self)
|
|
114
|
+
self.openMapAction.setShortcut("Ctrl+O")
|
|
115
|
+
self.openMapAction.setStatusTip("Open a behaviors coding map")
|
|
116
|
+
self.openMapAction.triggered.connect(self.openMap)
|
|
117
|
+
|
|
118
|
+
self.saveMapAction = QAction(QIcon(), "&Save the behavior coding map", self)
|
|
119
|
+
self.saveMapAction.setShortcut("Ctrl+S")
|
|
120
|
+
self.saveMapAction.setStatusTip("Save the behavior coding map")
|
|
121
|
+
self.saveMapAction.setEnabled(False)
|
|
122
|
+
self.saveMapAction.triggered.connect(self.saveMap_clicked)
|
|
123
|
+
|
|
124
|
+
self.saveAsMapAction = QAction(QIcon(), "Save the behavior coding map as ...", self)
|
|
125
|
+
self.saveAsMapAction.setStatusTip("Save the behavior coding map as ...")
|
|
126
|
+
self.saveAsMapAction.setEnabled(False)
|
|
127
|
+
self.saveAsMapAction.triggered.connect(self.saveAsMap_clicked)
|
|
128
|
+
|
|
129
|
+
self.mapNameAction = QAction(QIcon(), "&Edit name of behaviors coding map", self)
|
|
130
|
+
self.mapNameAction.setShortcut("Ctrl+M")
|
|
131
|
+
self.mapNameAction.setStatusTip("Edit name of behaviors coding map")
|
|
132
|
+
self.mapNameAction.setEnabled(False)
|
|
133
|
+
self.mapNameAction.triggered.connect(self.mapName_clicked)
|
|
134
|
+
|
|
135
|
+
self.resizeAction = QAction(QIcon(), "Resize the image", self)
|
|
136
|
+
self.resizeAction.setStatusTip("Resize the image")
|
|
137
|
+
self.resizeAction.setEnabled(False)
|
|
138
|
+
self.resizeAction.triggered.connect(self.resize_clicked)
|
|
139
|
+
|
|
140
|
+
self.addToProject = QAction(QIcon(), "Add coding map to project", self)
|
|
141
|
+
self.addToProject.setStatusTip("Add coding map to project")
|
|
142
|
+
self.addToProject.setEnabled(False)
|
|
143
|
+
self.addToProject.triggered.connect(self.add_to_project)
|
|
144
|
+
|
|
145
|
+
self.exitAction = QAction(QIcon(), "&Close", self)
|
|
146
|
+
self.exitAction.setStatusTip(cfg.CLOSE)
|
|
147
|
+
self.exitAction.triggered.connect(self.close)
|
|
148
|
+
|
|
149
|
+
menubar = self.menuBar()
|
|
150
|
+
fileMenu = menubar.addMenu("&File")
|
|
151
|
+
fileMenu.addAction(self.newMapAction)
|
|
152
|
+
fileMenu.addAction(self.openMapAction)
|
|
153
|
+
fileMenu.addAction(self.saveMapAction)
|
|
154
|
+
fileMenu.addAction(self.saveAsMapAction)
|
|
155
|
+
fileMenu.addSeparator()
|
|
156
|
+
fileMenu.addAction(self.mapNameAction)
|
|
157
|
+
fileMenu.addSeparator()
|
|
158
|
+
fileMenu.addAction(self.resizeAction)
|
|
159
|
+
fileMenu.addSeparator()
|
|
160
|
+
fileMenu.addAction(self.addToProject)
|
|
161
|
+
fileMenu.addSeparator()
|
|
162
|
+
fileMenu.addAction(self.exitAction)
|
|
163
|
+
|
|
164
|
+
splitter1 = QSplitter(Qt.Vertical)
|
|
165
|
+
|
|
166
|
+
self.view = self.View(self)
|
|
167
|
+
self.view.mousePress.connect(self.viewMousePressEvent)
|
|
168
|
+
splitter1.addWidget(self.view)
|
|
169
|
+
|
|
170
|
+
vlayout_list = QVBoxLayout()
|
|
171
|
+
vlayout_list.addWidget(QLabel("Defined area"))
|
|
172
|
+
|
|
173
|
+
self.area_list = QListWidget(self)
|
|
174
|
+
self.area_list.itemClicked.connect(self.area_list_item_click)
|
|
175
|
+
vlayout_list.addWidget(self.area_list)
|
|
176
|
+
w = QWidget()
|
|
177
|
+
w.setLayout(vlayout_list)
|
|
178
|
+
splitter1.addWidget(w)
|
|
179
|
+
splitter1.setSizes([300, 100])
|
|
180
|
+
splitter1.setStretchFactor(2, 8)
|
|
181
|
+
|
|
182
|
+
hlayout_cmd = QHBoxLayout()
|
|
183
|
+
|
|
184
|
+
self.btNewArea = QPushButton("New behavior area", self)
|
|
185
|
+
self.btNewArea.clicked.connect(self.newArea)
|
|
186
|
+
self.btNewArea.setVisible(False)
|
|
187
|
+
hlayout_cmd.addWidget(self.btNewArea)
|
|
188
|
+
|
|
189
|
+
self.btSaveArea = QPushButton("Save the behavior area", self)
|
|
190
|
+
self.btSaveArea.clicked.connect(self.saveArea)
|
|
191
|
+
self.btSaveArea.setVisible(False)
|
|
192
|
+
hlayout_cmd.addWidget(self.btSaveArea)
|
|
193
|
+
|
|
194
|
+
self.btCancelAreaCreation = QPushButton("Cancel", self)
|
|
195
|
+
self.btCancelAreaCreation.clicked.connect(self.cancelAreaCreation)
|
|
196
|
+
self.btCancelAreaCreation.setVisible(False)
|
|
197
|
+
hlayout_cmd.addWidget(self.btCancelAreaCreation)
|
|
198
|
+
|
|
199
|
+
hlayout_cmd.addItem(QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum))
|
|
200
|
+
|
|
201
|
+
self.btDeleteArea = QPushButton("Delete selected behavior area", self)
|
|
202
|
+
self.btDeleteArea.clicked.connect(self.deleteArea)
|
|
203
|
+
self.btDeleteArea.setVisible(False)
|
|
204
|
+
hlayout_cmd.addWidget(self.btDeleteArea)
|
|
205
|
+
|
|
206
|
+
hlayout_area = QHBoxLayout()
|
|
207
|
+
|
|
208
|
+
self.lb = QLabel("Behavior")
|
|
209
|
+
self.lb.setVisible(False)
|
|
210
|
+
hlayout_area.addWidget(self.lb)
|
|
211
|
+
|
|
212
|
+
self.leAreaCode = QLineEdit(self)
|
|
213
|
+
self.leAreaCode.setReadOnly(True)
|
|
214
|
+
self.leAreaCode.setVisible(False)
|
|
215
|
+
self.leAreaCode.setEnabled(False)
|
|
216
|
+
hlayout_area.addWidget(self.leAreaCode)
|
|
217
|
+
|
|
218
|
+
self.btEditAreaCode = QPushButton("Select behavior")
|
|
219
|
+
self.btEditAreaCode.clicked.connect(self.edit_area_code)
|
|
220
|
+
self.btEditAreaCode.setVisible(False)
|
|
221
|
+
hlayout_area.addWidget(self.btEditAreaCode)
|
|
222
|
+
|
|
223
|
+
self.btColor = QPushButton(clicked=self.chooseColor)
|
|
224
|
+
self.btColor.setVisible(False)
|
|
225
|
+
self.btColor.setStyleSheet(f"QWidget {{background-color:{self.areaColor.name()}}}")
|
|
226
|
+
hlayout_area.addWidget(self.btColor)
|
|
227
|
+
|
|
228
|
+
self.slAlpha = QSlider(Qt.Horizontal)
|
|
229
|
+
self.slAlpha.setRange(20, 100)
|
|
230
|
+
self.slAlpha.setValue(50)
|
|
231
|
+
self.slAlpha.valueChanged.connect(self.slAlpha_changed)
|
|
232
|
+
self.slAlpha.setVisible(False)
|
|
233
|
+
hlayout_area.addWidget(self.slAlpha)
|
|
234
|
+
|
|
235
|
+
self.slAlpha_changed(50)
|
|
236
|
+
|
|
237
|
+
vlayout_frame = QVBoxLayout()
|
|
238
|
+
vlayout_frame.addLayout(hlayout_cmd)
|
|
239
|
+
vlayout_frame.addLayout(hlayout_area)
|
|
240
|
+
vlayout_frame.addItem(QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding))
|
|
241
|
+
|
|
242
|
+
frame = QFrame()
|
|
243
|
+
frame.setFrameStyle(QFrame.Panel | QFrame.Plain)
|
|
244
|
+
frame.setMinimumHeight(120)
|
|
245
|
+
frame.setMaximumHeight(120)
|
|
246
|
+
|
|
247
|
+
frame.setLayout(vlayout_frame)
|
|
248
|
+
|
|
249
|
+
vlayout = QVBoxLayout()
|
|
250
|
+
|
|
251
|
+
vlayout.addWidget(splitter1)
|
|
252
|
+
vlayout.addWidget(frame)
|
|
253
|
+
|
|
254
|
+
main_widget = QWidget(self)
|
|
255
|
+
main_widget.setLayout(vlayout)
|
|
256
|
+
self.setCentralWidget(main_widget)
|
|
257
|
+
|
|
258
|
+
self.statusBar().showMessage("")
|
|
259
|
+
|
|
260
|
+
def add_to_project(self, item):
|
|
261
|
+
"""
|
|
262
|
+
add coding map to project
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
map_dict = self.make_coding_map_dict()
|
|
266
|
+
if map_dict["areas"] == {}:
|
|
267
|
+
QMessageBox.critical(
|
|
268
|
+
self,
|
|
269
|
+
cfg.programName,
|
|
270
|
+
(
|
|
271
|
+
"The behaviors coding map does not contain any behavior area.<br>"
|
|
272
|
+
"Add some behavior areas before adding the coding map to the project"
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
self.signal_add_to_project.emit(map_dict)
|
|
279
|
+
|
|
280
|
+
def area_list_item_click(self, item):
|
|
281
|
+
"""
|
|
282
|
+
select the polygon corresponding to the clicked area
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
if self.selectedPolygon:
|
|
286
|
+
self.selectedPolygon.setPen(QPen(designColor, penWidth, penStyle, Qt.RoundCap, Qt.RoundJoin))
|
|
287
|
+
self.selectedPolygon = None
|
|
288
|
+
self.selectedPolygonMemBrush = None
|
|
289
|
+
|
|
290
|
+
idx = int(item.text().split("#")[1])
|
|
291
|
+
ac, pg = self.polygonsList2[idx]
|
|
292
|
+
|
|
293
|
+
self.selectedPolygon = pg
|
|
294
|
+
|
|
295
|
+
self.selectedPolygonMemBrush = self.selectedPolygon.brush()
|
|
296
|
+
|
|
297
|
+
self.selectedPolygon.setPen(QPen(QColor(255, 0, 0, 255), 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
|
|
298
|
+
|
|
299
|
+
self.leAreaCode.setText(ac)
|
|
300
|
+
|
|
301
|
+
for widget in (
|
|
302
|
+
self.lb,
|
|
303
|
+
self.leAreaCode,
|
|
304
|
+
self.btEditAreaCode,
|
|
305
|
+
self.btDeleteArea,
|
|
306
|
+
self.btColor,
|
|
307
|
+
self.slAlpha,
|
|
308
|
+
):
|
|
309
|
+
widget.setVisible(True)
|
|
310
|
+
|
|
311
|
+
self.areaColor = self.selectedPolygon.brush().color()
|
|
312
|
+
self.btColor.setStyleSheet(f"QWidget {{background-color:{self.selectedPolygon.brush().color().name()}}}")
|
|
313
|
+
self.slAlpha.setValue(int(self.areaColor.alpha() / 255 * 100))
|
|
314
|
+
|
|
315
|
+
def edit_area_code(self):
|
|
316
|
+
"""
|
|
317
|
+
select a behavior
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
if self.leAreaCode.text() in self.codes_list:
|
|
321
|
+
code_index = self.codes_list.index(self.leAreaCode.text())
|
|
322
|
+
else:
|
|
323
|
+
code_index = 0
|
|
324
|
+
|
|
325
|
+
item, ok = QInputDialog.getItem(self, "Select a behavior", "Available behaviors", self.codes_list, code_index, False)
|
|
326
|
+
self.leAreaCode.setText(item)
|
|
327
|
+
|
|
328
|
+
if self.selectedPolygon:
|
|
329
|
+
for idx, area in enumerate(self.polygonsList2):
|
|
330
|
+
ac, pg = area
|
|
331
|
+
if pg == self.selectedPolygon:
|
|
332
|
+
self.polygonsList2[idx] = [self.leAreaCode.text(), pg]
|
|
333
|
+
break
|
|
334
|
+
|
|
335
|
+
self.update_area_list()
|
|
336
|
+
|
|
337
|
+
def slAlpha_changed(self, val):
|
|
338
|
+
"""
|
|
339
|
+
opacity slider value changed
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
self.btColor.setText(f"Opacity: {val} %")
|
|
343
|
+
self.areaColor.setAlpha(int(val / 100 * 255))
|
|
344
|
+
|
|
345
|
+
if self.selectedPolygon:
|
|
346
|
+
self.selectedPolygon.setBrush(self.areaColor)
|
|
347
|
+
for idx, area in enumerate(self.polygonsList2):
|
|
348
|
+
ac, pg = area
|
|
349
|
+
if pg == self.selectedPolygon:
|
|
350
|
+
pg.setBrush(self.areaColor)
|
|
351
|
+
self.polygonsList2[idx] = [ac, pg]
|
|
352
|
+
break
|
|
353
|
+
|
|
354
|
+
if self.closedPolygon:
|
|
355
|
+
self.closedPolygon.setBrush(self.areaColor)
|
|
356
|
+
|
|
357
|
+
def chooseColor(self):
|
|
358
|
+
"""
|
|
359
|
+
area color button clicked
|
|
360
|
+
"""
|
|
361
|
+
cd = QColorDialog()
|
|
362
|
+
cd.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
363
|
+
cd.setOptions(QColorDialog.DontUseNativeDialog)
|
|
364
|
+
|
|
365
|
+
if cd.exec():
|
|
366
|
+
self.areaColor = cd.currentColor()
|
|
367
|
+
self.btColor.setStyleSheet(f"QWidget {{background-color:{self.areaColor.name()}}}")
|
|
368
|
+
self.areaColor.setAlpha(int(self.slAlpha.value() / 100 * 255))
|
|
369
|
+
|
|
370
|
+
if self.selectedPolygon:
|
|
371
|
+
self.selectedPolygon.setBrush(self.areaColor)
|
|
372
|
+
|
|
373
|
+
for idx, area in enumerate(self.polygonsList2):
|
|
374
|
+
ac, pg = area
|
|
375
|
+
if pg == self.selectedPolygon:
|
|
376
|
+
pg.setBrush(self.areaColor)
|
|
377
|
+
self.polygonsList2[idx] = [ac, pg]
|
|
378
|
+
break
|
|
379
|
+
|
|
380
|
+
if self.closedPolygon:
|
|
381
|
+
self.closedPolygon.setBrush(self.areaColor)
|
|
382
|
+
|
|
383
|
+
def closeEvent(self, event):
|
|
384
|
+
if self.flag_map_changed:
|
|
385
|
+
response = dialog.MessageDialog(
|
|
386
|
+
"BORIS - Behaviors map creator",
|
|
387
|
+
"What to do about the current unsaved behaviors coding map?",
|
|
388
|
+
[cfg.SAVE, cfg.DISCARD, cfg.CANCEL],
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
if response == cfg.SAVE:
|
|
392
|
+
if not self.saveMap_clicked():
|
|
393
|
+
event.ignore()
|
|
394
|
+
|
|
395
|
+
if response == cfg.CANCEL:
|
|
396
|
+
event.ignore()
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
self.flag_map_changed = False
|
|
400
|
+
event.accept()
|
|
401
|
+
|
|
402
|
+
def viewMousePressEvent(self, event):
|
|
403
|
+
"""
|
|
404
|
+
check if area selected with mouse
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
def add_polygon():
|
|
408
|
+
# create polygon
|
|
409
|
+
newPolygon = QPolygonF()
|
|
410
|
+
for p in self.view.points:
|
|
411
|
+
newPolygon.append(QPoint(p[0], p[1]))
|
|
412
|
+
|
|
413
|
+
# draw polygon a red polygon
|
|
414
|
+
self.closedPolygon = QGraphicsPolygonItem(newPolygon)
|
|
415
|
+
self.closedPolygon.setPen(QPen(designColor, penWidth, penStyle, Qt.RoundCap, Qt.RoundJoin))
|
|
416
|
+
self.closedPolygon.setBrush(self.areaColor)
|
|
417
|
+
self.view.scene().addItem(self.closedPolygon)
|
|
418
|
+
|
|
419
|
+
if not self.bitmapFileName:
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
test = self.view.mapToScene(event.pos()).toPoint() # coordinates of clicked point
|
|
423
|
+
|
|
424
|
+
if test.x() < 0 or test.y() < 0 or test.x() > self.pixmap.size().width() or test.y() > self.pixmap.size().height():
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
if not self.flagNewArea: # test clicked point for areas
|
|
428
|
+
# reset selected polygon to default pen
|
|
429
|
+
if self.selectedPolygon:
|
|
430
|
+
self.selectedPolygon.setPen(QPen(designColor, penWidth, penStyle, Qt.RoundCap, Qt.RoundJoin))
|
|
431
|
+
self.selectedPolygon = None
|
|
432
|
+
self.selectedPolygonMemBrush = None
|
|
433
|
+
|
|
434
|
+
idx = 0
|
|
435
|
+
for areaCode, pg in self.polygonsList2:
|
|
436
|
+
if pg.contains(test):
|
|
437
|
+
self.selectedPolygon = pg
|
|
438
|
+
self.selectedPolygonMemBrush = self.selectedPolygon.brush()
|
|
439
|
+
self.selectedPolygon.setPen(QPen(QColor(255, 0, 0, 255), 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
|
|
440
|
+
self.leAreaCode.setText(areaCode)
|
|
441
|
+
|
|
442
|
+
for widget in (self.lb, self.leAreaCode, self.btEditAreaCode, self.btColor, self.slAlpha, self.btDeleteArea):
|
|
443
|
+
widget.setVisible(True)
|
|
444
|
+
|
|
445
|
+
self.areaColor = self.selectedPolygon.brush().color()
|
|
446
|
+
self.btColor.setStyleSheet(f"QWidget {{background-color:{self.selectedPolygon.brush().color().name()}}}")
|
|
447
|
+
|
|
448
|
+
self.slAlpha.setValue(int(self.areaColor.alpha() / 255 * 100))
|
|
449
|
+
|
|
450
|
+
# select area in list widget
|
|
451
|
+
item = self.area_list.item(idx)
|
|
452
|
+
self.area_list.setCurrentItem(item)
|
|
453
|
+
|
|
454
|
+
break
|
|
455
|
+
idx += 1
|
|
456
|
+
|
|
457
|
+
if not self.selectedPolygon:
|
|
458
|
+
self.leAreaCode.setVisible(False)
|
|
459
|
+
self.lb.setVisible(False)
|
|
460
|
+
self.btDeleteArea.setVisible(False)
|
|
461
|
+
self.btEditAreaCode.setVisible(False)
|
|
462
|
+
self.btColor.setVisible(False)
|
|
463
|
+
self.slAlpha.setVisible(False)
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
# delete last line item
|
|
467
|
+
if (event.buttons() & Qt.RightButton) and not self.closedPolygon:
|
|
468
|
+
if self.view.points:
|
|
469
|
+
self.view.points = self.view.points[0:-1]
|
|
470
|
+
|
|
471
|
+
if self.view.points:
|
|
472
|
+
self.view._start = QPoint(self.view.points[-1][0], self.view.points[-1][1])
|
|
473
|
+
else:
|
|
474
|
+
self.view._start = None
|
|
475
|
+
|
|
476
|
+
# remove graphical elements
|
|
477
|
+
if self.view.elList:
|
|
478
|
+
self.view.scene().removeItem(self.view.elList[-1])
|
|
479
|
+
self.view.elList = self.view.elList[0:-1]
|
|
480
|
+
|
|
481
|
+
# middle button automatically close the polygon
|
|
482
|
+
if (event.buttons() & Qt.MiddleButton) and not self.closedPolygon:
|
|
483
|
+
line = QGraphicsLineItem(QLineF(self.view._start, QPoint(self.view.points[0][0], self.view.points[0][1])))
|
|
484
|
+
line.setPen(QPen(designColor, penWidth, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
|
|
485
|
+
|
|
486
|
+
self.view.scene().addItem(line)
|
|
487
|
+
self.view.elList.append(line)
|
|
488
|
+
|
|
489
|
+
self.statusBar().showMessage("Area completed")
|
|
490
|
+
|
|
491
|
+
# create polygon
|
|
492
|
+
add_polygon()
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
# add line item
|
|
496
|
+
if event.buttons() == Qt.LeftButton and not self.closedPolygon:
|
|
497
|
+
if self.view._start:
|
|
498
|
+
end = test
|
|
499
|
+
|
|
500
|
+
# test is polygon is crossed
|
|
501
|
+
if len(self.view.points) >= 3:
|
|
502
|
+
for idx, _ in enumerate(self.view.points[:-2]):
|
|
503
|
+
if util.intersection(
|
|
504
|
+
self.view.points[idx],
|
|
505
|
+
self.view.points[idx + 1],
|
|
506
|
+
self.view.points[-1],
|
|
507
|
+
(int(end.x()), int(end.y())),
|
|
508
|
+
):
|
|
509
|
+
QMessageBox.critical(self, "", "The polygon edges can not be intersected")
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
# test if polygon closed (dist min 10 px)
|
|
513
|
+
if abs(end.x() - self.view.points[0][0]) < 10 and abs(end.y() - self.view.points[0][1]) < 10:
|
|
514
|
+
line = QGraphicsLineItem(QLineF(self.view._start, QPoint(self.view.points[0][0], self.view.points[0][1])))
|
|
515
|
+
line.setPen(QPen(designColor, penWidth, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
|
|
516
|
+
|
|
517
|
+
self.view.scene().addItem(line)
|
|
518
|
+
self.view.elList.append(line)
|
|
519
|
+
|
|
520
|
+
self.statusBar().showMessage("Area completed")
|
|
521
|
+
|
|
522
|
+
# create polygon
|
|
523
|
+
add_polygon()
|
|
524
|
+
return
|
|
525
|
+
|
|
526
|
+
self.view.points.append((int(end.x()), int(end.y())))
|
|
527
|
+
|
|
528
|
+
line = QGraphicsLineItem(QLineF(self.view._start, end))
|
|
529
|
+
line.setPen(QPen(designColor, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
|
|
530
|
+
self.view.scene().addItem(line)
|
|
531
|
+
self.view.elList.append(line)
|
|
532
|
+
|
|
533
|
+
self.view._start = test
|
|
534
|
+
|
|
535
|
+
else: # first point
|
|
536
|
+
self.view._start = test
|
|
537
|
+
|
|
538
|
+
ellipse = QGraphicsEllipseItem(self.view._start.x(), self.view._start.y(), 3, 3)
|
|
539
|
+
ellipse.setPen(QPen(designColor, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
|
|
540
|
+
|
|
541
|
+
brush = QBrush()
|
|
542
|
+
brush.setStyle(Qt.SolidPattern)
|
|
543
|
+
brush.setColor(designColor)
|
|
544
|
+
ellipse.setBrush(brush)
|
|
545
|
+
|
|
546
|
+
self.view.scene().addItem(ellipse)
|
|
547
|
+
self.view.elList.append(ellipse)
|
|
548
|
+
|
|
549
|
+
self.view.points.append((self.view._start.x(), self.view._start.y()))
|
|
550
|
+
|
|
551
|
+
# automatically close the polygon
|
|
552
|
+
if event.buttons() == Qt.MiddleButton and not self.closedPolygon:
|
|
553
|
+
# add first point as last point of polygon
|
|
554
|
+
|
|
555
|
+
# test is polygon is crossed
|
|
556
|
+
|
|
557
|
+
if len(self.view.points) >= 3:
|
|
558
|
+
for idx, _ in enumerate(self.view.points[1:-2]):
|
|
559
|
+
if util.intersection(
|
|
560
|
+
self.view.points[idx],
|
|
561
|
+
self.view.points[idx + 1],
|
|
562
|
+
self.view.points[-1],
|
|
563
|
+
self.view.points[0],
|
|
564
|
+
):
|
|
565
|
+
QMessageBox.critical(self, "", "The polygon edges can not be intersected")
|
|
566
|
+
return
|
|
567
|
+
|
|
568
|
+
line = QGraphicsLineItem(
|
|
569
|
+
QLineF(self.view.points[-1][0], self.view.points[-1][1], self.view.points[0][0], self.view.points[0][1])
|
|
570
|
+
)
|
|
571
|
+
line.setPen(QPen(designColor, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
|
|
572
|
+
self.view.scene().addItem(line)
|
|
573
|
+
self.view.elList.append(line)
|
|
574
|
+
|
|
575
|
+
self.view.points.append(self.view.points[0])
|
|
576
|
+
add_polygon()
|
|
577
|
+
self.statusBar().showMessage("Area completed")
|
|
578
|
+
|
|
579
|
+
def mapName_clicked(self):
|
|
580
|
+
"""
|
|
581
|
+
Edit map name
|
|
582
|
+
"""
|
|
583
|
+
|
|
584
|
+
while True:
|
|
585
|
+
map_name, ok = QInputDialog.getText(
|
|
586
|
+
self, "Behaviors coding map name", "Enter a name for the coding map", QLineEdit.Normal, self.mapName
|
|
587
|
+
)
|
|
588
|
+
if map_name.upper() in self.bcm_list:
|
|
589
|
+
QMessageBox.critical(
|
|
590
|
+
self,
|
|
591
|
+
"",
|
|
592
|
+
(
|
|
593
|
+
"The name for the new coding map already exists.<br>"
|
|
594
|
+
f"{', '.join(self.bcm_list)} are already defined.<br>"
|
|
595
|
+
"To reuse the same name the existing coding map must be deleted (File > Edit project)"
|
|
596
|
+
),
|
|
597
|
+
)
|
|
598
|
+
if ok and map_name and map_name.upper() not in self.bcm_list:
|
|
599
|
+
self.mapName = map_name
|
|
600
|
+
self.setWindowTitle(f"{cfg.programName} - Behaviors coding map creator - {self.mapName}")
|
|
601
|
+
break
|
|
602
|
+
if not ok:
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
def newMap(self):
|
|
606
|
+
"""
|
|
607
|
+
create a new map
|
|
608
|
+
"""
|
|
609
|
+
|
|
610
|
+
if self.flag_map_changed:
|
|
611
|
+
response = dialog.MessageDialog(
|
|
612
|
+
cfg.programName + " - Behaviors coding map creator",
|
|
613
|
+
"What to do about the current unsaved coding map?",
|
|
614
|
+
[cfg.SAVE, cfg.DISCARD, cfg.CANCEL],
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
if response == cfg.SAVE:
|
|
618
|
+
if not self.saveMap_clicked():
|
|
619
|
+
return
|
|
620
|
+
|
|
621
|
+
if response == cfg.CANCEL:
|
|
622
|
+
return
|
|
623
|
+
|
|
624
|
+
self.cancelMap()
|
|
625
|
+
|
|
626
|
+
while True:
|
|
627
|
+
map_name, ok = QInputDialog.getText(self, "Behaviors coding map name", "Enter a name for the new coding map")
|
|
628
|
+
if not ok:
|
|
629
|
+
return
|
|
630
|
+
if map_name.upper() in self.bcm_list:
|
|
631
|
+
QMessageBox.critical(
|
|
632
|
+
self,
|
|
633
|
+
"",
|
|
634
|
+
(
|
|
635
|
+
"The name for the new coding map already exists.<br>"
|
|
636
|
+
f"{', '.join(self.bcm_list)} are already defined.<br>"
|
|
637
|
+
"To reuse the same name the existing coding map must be deleted (File > Edit project)"
|
|
638
|
+
),
|
|
639
|
+
)
|
|
640
|
+
if ok and map_name and map_name.upper() not in self.bcm_list:
|
|
641
|
+
self.mapName = map_name
|
|
642
|
+
break
|
|
643
|
+
|
|
644
|
+
self.setWindowTitle(f"{cfg.programName} - Behaviors coding map creator tool - {self.mapName}")
|
|
645
|
+
|
|
646
|
+
self.loadBitmap()
|
|
647
|
+
|
|
648
|
+
def openMap(self):
|
|
649
|
+
"""
|
|
650
|
+
open a coding map from file
|
|
651
|
+
|
|
652
|
+
load bitmap from data
|
|
653
|
+
show it in view scene
|
|
654
|
+
"""
|
|
655
|
+
if self.flag_map_changed:
|
|
656
|
+
response = dialog.MessageDialog(
|
|
657
|
+
cfg.programName + " - Behaviors coding map creator",
|
|
658
|
+
"What to do about the current unsaved coding map?",
|
|
659
|
+
["Save", "Discard", "Cancel"],
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
if (response == "Save" and not self.saveMap_clicked()) or (response == "Cancel"):
|
|
663
|
+
return
|
|
664
|
+
|
|
665
|
+
fileName, _ = QFileDialog(self).getOpenFileName(
|
|
666
|
+
self, "Open a behaviors coding map", "", "Behaviors coding map (*.behav_coding_map);;All files (*)"
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
if not fileName:
|
|
670
|
+
return
|
|
671
|
+
try:
|
|
672
|
+
self.codingMap = json.loads(open(fileName, "r").read())
|
|
673
|
+
except Exception:
|
|
674
|
+
QMessageBox.critical(self, cfg.programName, f"The file {fileName} is not a behaviors coding map.")
|
|
675
|
+
return
|
|
676
|
+
|
|
677
|
+
if "coding_map_type" not in self.codingMap or self.codingMap["coding_map_type"] != "BORIS behaviors coding map":
|
|
678
|
+
QMessageBox.critical(self, cfg.programName, f"The file {fileName} is not a BORIS behaviors coding map.")
|
|
679
|
+
return
|
|
680
|
+
|
|
681
|
+
self.cancelMap()
|
|
682
|
+
|
|
683
|
+
self.mapName = self.codingMap["name"]
|
|
684
|
+
|
|
685
|
+
self.setWindowTitle(f"{cfg.programName} - Behaviors coding map creator - {self.mapName}")
|
|
686
|
+
|
|
687
|
+
self.bitmapFileName = True
|
|
688
|
+
|
|
689
|
+
self.fileName = fileName
|
|
690
|
+
|
|
691
|
+
bitmapContent = binascii.a2b_base64(self.codingMap["bitmap"])
|
|
692
|
+
|
|
693
|
+
self.pixmap.loadFromData(bitmapContent)
|
|
694
|
+
|
|
695
|
+
self.view.setSceneRect(0, 0, self.pixmap.size().width(), self.pixmap.size().height())
|
|
696
|
+
self.view.setMinimumHeight(self.pixmap.size().height())
|
|
697
|
+
# self.view.setMaximumHeight(self.pixmap.size().height())
|
|
698
|
+
pixItem = QGraphicsPixmapItem(self.pixmap)
|
|
699
|
+
pixItem.setPos(0, 0)
|
|
700
|
+
self.view.scene().addItem(pixItem)
|
|
701
|
+
|
|
702
|
+
for key in self.codingMap["areas"]:
|
|
703
|
+
areaCode = self.codingMap["areas"][key]["code"]
|
|
704
|
+
points = self.codingMap["areas"][key]["geometry"]
|
|
705
|
+
|
|
706
|
+
newPolygon = QPolygonF()
|
|
707
|
+
for p in points:
|
|
708
|
+
newPolygon.append(QPoint(p[0], p[1]))
|
|
709
|
+
|
|
710
|
+
# draw polygon
|
|
711
|
+
polygon = QGraphicsPolygonItem()
|
|
712
|
+
polygon.setPolygon(newPolygon)
|
|
713
|
+
clr = QColor()
|
|
714
|
+
clr.setRgba(self.codingMap["areas"][key]["color"])
|
|
715
|
+
polygon.setPen(QPen(clr, penWidth, penStyle, Qt.RoundCap, Qt.RoundJoin))
|
|
716
|
+
polygon.setBrush(QBrush(clr, Qt.SolidPattern))
|
|
717
|
+
|
|
718
|
+
self.view.scene().addItem(polygon)
|
|
719
|
+
|
|
720
|
+
self.polygonsList2.append([areaCode, polygon])
|
|
721
|
+
|
|
722
|
+
self.btNewArea.setVisible(True)
|
|
723
|
+
|
|
724
|
+
for action in (
|
|
725
|
+
self.saveMapAction,
|
|
726
|
+
self.saveAsMapAction,
|
|
727
|
+
self.addToProject,
|
|
728
|
+
self.mapNameAction,
|
|
729
|
+
self.resizeAction,
|
|
730
|
+
):
|
|
731
|
+
action.setEnabled(True)
|
|
732
|
+
|
|
733
|
+
self.update_area_list()
|
|
734
|
+
|
|
735
|
+
def make_coding_map_dict(self) -> dict:
|
|
736
|
+
"""
|
|
737
|
+
create the coding map dictionary
|
|
738
|
+
the image is encoded in bse64 format
|
|
739
|
+
"""
|
|
740
|
+
map_dict = {"coding_map_type": "BORIS behaviors coding map", "name": self.mapName, "areas": {}}
|
|
741
|
+
|
|
742
|
+
for ac, pg in self.polygonsList2:
|
|
743
|
+
if not map_dict["areas"]:
|
|
744
|
+
idx = 0
|
|
745
|
+
else:
|
|
746
|
+
idx = max(map_dict["areas"].keys()) + 1
|
|
747
|
+
|
|
748
|
+
points = []
|
|
749
|
+
for p in range(pg.polygon().count()):
|
|
750
|
+
points.append([int(pg.polygon().value(p).x()), int(pg.polygon().value(p).y())])
|
|
751
|
+
|
|
752
|
+
map_dict["areas"][idx] = {"code": ac, "geometry": points, "color": pg.brush().color().rgba()}
|
|
753
|
+
|
|
754
|
+
map_dict["areas"] = json.loads(json.dumps(map_dict["areas"]))
|
|
755
|
+
|
|
756
|
+
# Save QPixmap to QByteArray via QBuffer.
|
|
757
|
+
byte_array = QByteArray()
|
|
758
|
+
buffer = QBuffer(byte_array)
|
|
759
|
+
buffer.open(QIODevice.WriteOnly)
|
|
760
|
+
self.pixmap.save(buffer, "PNG")
|
|
761
|
+
string_io = io.BytesIO(byte_array)
|
|
762
|
+
string_io.seek(0)
|
|
763
|
+
|
|
764
|
+
# add codified bitmap
|
|
765
|
+
map_dict["bitmap"] = binascii.b2a_base64(string_io.read()).decode("utf-8")
|
|
766
|
+
|
|
767
|
+
return map_dict
|
|
768
|
+
|
|
769
|
+
def saveMap(self):
|
|
770
|
+
"""
|
|
771
|
+
save current coding map in JSON format
|
|
772
|
+
"""
|
|
773
|
+
|
|
774
|
+
if self.fileName:
|
|
775
|
+
mapDict = self.make_coding_map_dict()
|
|
776
|
+
|
|
777
|
+
with open(self.fileName, "w") as outfile:
|
|
778
|
+
outfile.write(json.dumps(mapDict))
|
|
779
|
+
|
|
780
|
+
self.flag_map_changed = False
|
|
781
|
+
|
|
782
|
+
return True
|
|
783
|
+
else:
|
|
784
|
+
return False
|
|
785
|
+
|
|
786
|
+
def saveAsMap_clicked(self):
|
|
787
|
+
filters = "Behaviors coding map (*.behav_coding_map);;All files (*)"
|
|
788
|
+
|
|
789
|
+
self.fileName, _ = QFileDialog.getSaveFileName(self, "Save behaviors coding map as", "", filters)
|
|
790
|
+
|
|
791
|
+
if not self.fileName:
|
|
792
|
+
return
|
|
793
|
+
if Path(self.fileName).suffix != ".behav_coding_map":
|
|
794
|
+
self.fileName += ".behav_coding_map"
|
|
795
|
+
self.saveMap()
|
|
796
|
+
|
|
797
|
+
def saveMap_clicked(self):
|
|
798
|
+
if not self.fileName:
|
|
799
|
+
self.fileName, _ = QFileDialog().getSaveFileName(
|
|
800
|
+
self,
|
|
801
|
+
"Save modifiers map",
|
|
802
|
+
self.mapName + ".behav_coding_map",
|
|
803
|
+
"Behaviors coding map (*.behav_coding_map);;All files (*)",
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
if self.fileName and Path(self.fileName).suffix != ".behav_coding_map":
|
|
807
|
+
self.fileName += ".behav_coding_map"
|
|
808
|
+
|
|
809
|
+
if self.fileName:
|
|
810
|
+
return self.saveMap()
|
|
811
|
+
|
|
812
|
+
return False
|
|
813
|
+
|
|
814
|
+
def newArea(self):
|
|
815
|
+
if not self.bitmapFileName:
|
|
816
|
+
QMessageBox.critical(self, cfg.programName, "An image must be loaded before to define areas")
|
|
817
|
+
return
|
|
818
|
+
|
|
819
|
+
if self.selectedPolygon:
|
|
820
|
+
self.selectedPolygon.setPen(QPen(designColor, penWidth, penStyle, Qt.RoundCap, Qt.RoundJoin))
|
|
821
|
+
self.selectedPolygon = None
|
|
822
|
+
|
|
823
|
+
self.flagNewArea = True
|
|
824
|
+
self.btSaveArea.setVisible(True)
|
|
825
|
+
self.btCancelAreaCreation.setVisible(True)
|
|
826
|
+
self.btNewArea.setVisible(False)
|
|
827
|
+
self.lb.setVisible(True)
|
|
828
|
+
self.leAreaCode.clear()
|
|
829
|
+
self.leAreaCode.setVisible(True)
|
|
830
|
+
# self.leAreaCode.setEnabled(False)
|
|
831
|
+
self.btEditAreaCode.setVisible(True)
|
|
832
|
+
self.btColor.setVisible(True)
|
|
833
|
+
self.slAlpha.setVisible(True)
|
|
834
|
+
self.btDeleteArea.setVisible(False)
|
|
835
|
+
|
|
836
|
+
self.statusBar().showMessage(
|
|
837
|
+
("Click on bitmap to set the vertices of the area with the mouse (right click will cancel the last point)")
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
def saveArea(self):
|
|
841
|
+
if not self.closedPolygon:
|
|
842
|
+
QMessageBox.critical(
|
|
843
|
+
self,
|
|
844
|
+
cfg.programName,
|
|
845
|
+
("You must close your area before saving it.\nThe last vertex must correspond to the first one."),
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
if len(self.view.points) < 3:
|
|
849
|
+
QMessageBox.critical(self, cfg.programName, "You must define a closed area")
|
|
850
|
+
return
|
|
851
|
+
|
|
852
|
+
# check if no area code
|
|
853
|
+
if not self.leAreaCode.text():
|
|
854
|
+
QMessageBox.critical(self, cfg.programName, "You must define a code for the new behavior area")
|
|
855
|
+
return
|
|
856
|
+
|
|
857
|
+
# remove all lines
|
|
858
|
+
for x in self.view.elList:
|
|
859
|
+
self.view.scene().removeItem(x)
|
|
860
|
+
|
|
861
|
+
# draw polygon
|
|
862
|
+
self.closedPolygon.setBrush(QBrush(self.areaColor, Qt.SolidPattern))
|
|
863
|
+
self.polygonsList2.append([self.leAreaCode.text(), self.closedPolygon])
|
|
864
|
+
|
|
865
|
+
self.flagNewArea = None
|
|
866
|
+
self.closedPolygon = None
|
|
867
|
+
self.view._start = 0
|
|
868
|
+
self.view.points = []
|
|
869
|
+
self.view.elList = []
|
|
870
|
+
|
|
871
|
+
for widget in (
|
|
872
|
+
self.btSaveArea,
|
|
873
|
+
self.btCancelAreaCreation,
|
|
874
|
+
self.lb,
|
|
875
|
+
self.leAreaCode,
|
|
876
|
+
self.btEditAreaCode,
|
|
877
|
+
self.btColor,
|
|
878
|
+
self.slAlpha,
|
|
879
|
+
self.btDeleteArea,
|
|
880
|
+
self.btNewArea,
|
|
881
|
+
):
|
|
882
|
+
widget.setVisible(False)
|
|
883
|
+
|
|
884
|
+
self.btNewArea.setVisible(True)
|
|
885
|
+
|
|
886
|
+
self.leAreaCode.setText("")
|
|
887
|
+
|
|
888
|
+
self.update_area_list()
|
|
889
|
+
|
|
890
|
+
self.flag_map_changed = True
|
|
891
|
+
self.statusBar().showMessage("New area saved", 5000)
|
|
892
|
+
|
|
893
|
+
def cancelAreaCreation(self):
|
|
894
|
+
if self.closedPolygon:
|
|
895
|
+
self.view.scene().removeItem(self.closedPolygon)
|
|
896
|
+
self.closedPolygon = None
|
|
897
|
+
|
|
898
|
+
# remove all lines
|
|
899
|
+
for x in self.view.elList:
|
|
900
|
+
self.view.scene().removeItem(x)
|
|
901
|
+
|
|
902
|
+
self.view.elList = []
|
|
903
|
+
|
|
904
|
+
self.view._start = 0
|
|
905
|
+
self.view.points = []
|
|
906
|
+
self.flagNewArea = False
|
|
907
|
+
self.btCancelAreaCreation.setVisible(False)
|
|
908
|
+
self.btDeleteArea.setVisible(False)
|
|
909
|
+
self.btSaveArea.setVisible(False)
|
|
910
|
+
self.lb.setVisible(False)
|
|
911
|
+
|
|
912
|
+
self.btColor.setVisible(False)
|
|
913
|
+
self.slAlpha.setVisible(False)
|
|
914
|
+
self.btNewArea.setVisible(True)
|
|
915
|
+
|
|
916
|
+
self.leAreaCode.setVisible(False)
|
|
917
|
+
self.leAreaCode.setText("")
|
|
918
|
+
|
|
919
|
+
self.btEditAreaCode.setVisible(False)
|
|
920
|
+
|
|
921
|
+
def update_area_list(self):
|
|
922
|
+
self.area_list.clear()
|
|
923
|
+
for idx, area in enumerate(self.polygonsList2):
|
|
924
|
+
ac, pg = area
|
|
925
|
+
self.area_list.addItem(f"{ac} #{idx}")
|
|
926
|
+
|
|
927
|
+
def deleteArea(self):
|
|
928
|
+
"""
|
|
929
|
+
remove selected area from map
|
|
930
|
+
"""
|
|
931
|
+
|
|
932
|
+
if self.selectedPolygon:
|
|
933
|
+
self.view.scene().removeItem(self.selectedPolygon)
|
|
934
|
+
|
|
935
|
+
to_delete = -1
|
|
936
|
+
for idx, area in enumerate(self.polygonsList2):
|
|
937
|
+
ac, pg = area
|
|
938
|
+
if pg == self.selectedPolygon:
|
|
939
|
+
to_delete = idx
|
|
940
|
+
|
|
941
|
+
if to_delete != -1:
|
|
942
|
+
del self.polygonsList2[to_delete]
|
|
943
|
+
|
|
944
|
+
self.flag_map_changed = True
|
|
945
|
+
|
|
946
|
+
self.view.elList = []
|
|
947
|
+
|
|
948
|
+
self.view._start = 0
|
|
949
|
+
self.view.points = []
|
|
950
|
+
self.flagNewArea = False
|
|
951
|
+
|
|
952
|
+
for widget in (self.btSaveArea, self.lb, self.btColor, self.slAlpha, self.leAreaCode, self.btDeleteArea):
|
|
953
|
+
widget.setVisible(False)
|
|
954
|
+
|
|
955
|
+
self.btNewArea.setVisible(True)
|
|
956
|
+
|
|
957
|
+
self.leAreaCode.setText("")
|
|
958
|
+
self.btEditAreaCode.setVisible(False)
|
|
959
|
+
|
|
960
|
+
self.statusBar().showMessage("")
|
|
961
|
+
|
|
962
|
+
self.update_area_list()
|
|
963
|
+
|
|
964
|
+
def cancelMap(self):
|
|
965
|
+
"""
|
|
966
|
+
remove current map
|
|
967
|
+
"""
|
|
968
|
+
self.flagNewArea = False
|
|
969
|
+
self.polygonsList2 = []
|
|
970
|
+
self.closedPolygon = None
|
|
971
|
+
self.selectedPolygon = None
|
|
972
|
+
self.area_list.clear()
|
|
973
|
+
self.view.scene().clear()
|
|
974
|
+
self.btDeleteArea.setVisible(False)
|
|
975
|
+
self.btNewArea.setVisible(False)
|
|
976
|
+
self.saveMapAction.setEnabled(False)
|
|
977
|
+
self.saveAsMapAction.setEnabled(False)
|
|
978
|
+
self.addToProject.setEnabled(False)
|
|
979
|
+
self.mapNameAction.setEnabled(False)
|
|
980
|
+
self.resizeAction.setEnabled(False)
|
|
981
|
+
self.statusBar().showMessage("")
|
|
982
|
+
|
|
983
|
+
self.btNewArea.setVisible(True)
|
|
984
|
+
self.btNewArea.setEnabled(True)
|
|
985
|
+
|
|
986
|
+
self.flag_map_changed = False
|
|
987
|
+
|
|
988
|
+
def loadBitmap(self):
|
|
989
|
+
"""
|
|
990
|
+
load bitmap as background for coding map
|
|
991
|
+
no more resize bitmap to CODING_MAP_RESIZE_W x CODING_MAP_RESIZE_H defined in config.py
|
|
992
|
+
"""
|
|
993
|
+
|
|
994
|
+
fileName, _ = QFileDialog.getOpenFileName(self, "Load bitmap", "", "bitmap files (*.png *.jpg);;All files (*)")
|
|
995
|
+
|
|
996
|
+
if not fileName:
|
|
997
|
+
return
|
|
998
|
+
self.bitmapFileName = fileName
|
|
999
|
+
|
|
1000
|
+
self.pixmap.load(self.bitmapFileName)
|
|
1001
|
+
|
|
1002
|
+
# scale image
|
|
1003
|
+
"""
|
|
1004
|
+
if (
|
|
1005
|
+
self.pixmap.size().width() > cfg.CODING_MAP_RESIZE_W
|
|
1006
|
+
or self.pixmap.size().height() > cfg.CODING_MAP_RESIZE_H
|
|
1007
|
+
):
|
|
1008
|
+
self.pixmap = self.pixmap.scaled(cfg.CODING_MAP_RESIZE_W, cfg.CODING_MAP_RESIZE_H, Qt.KeepAspectRatio)
|
|
1009
|
+
QMessageBox.information(
|
|
1010
|
+
self,
|
|
1011
|
+
cfg.programName,
|
|
1012
|
+
(
|
|
1013
|
+
f"The bitmap was resized to {self.pixmap.size().width()}x{self.pixmap.size().height()} pixels\n"
|
|
1014
|
+
"The original file was not modified"
|
|
1015
|
+
),
|
|
1016
|
+
)
|
|
1017
|
+
"""
|
|
1018
|
+
|
|
1019
|
+
self.view.setSceneRect(0, 0, self.pixmap.size().width(), self.pixmap.size().height())
|
|
1020
|
+
pixitem = QGraphicsPixmapItem(self.pixmap)
|
|
1021
|
+
pixitem.setPos(0, 0)
|
|
1022
|
+
self.view.scene().addItem(pixitem)
|
|
1023
|
+
|
|
1024
|
+
self.btNewArea.setVisible(True)
|
|
1025
|
+
|
|
1026
|
+
self.saveMapAction.setEnabled(True)
|
|
1027
|
+
self.saveAsMapAction.setEnabled(True)
|
|
1028
|
+
self.addToProject.setEnabled(True)
|
|
1029
|
+
self.mapNameAction.setEnabled(True)
|
|
1030
|
+
self.resizeAction.setEnabled(True)
|
|
1031
|
+
|
|
1032
|
+
self.statusBar().showMessage("""Click "New behavior area" to create a new behavior area""")
|
|
1033
|
+
|
|
1034
|
+
self.flag_map_changed = True
|
|
1035
|
+
|
|
1036
|
+
def resize_clicked(self):
|
|
1037
|
+
"""
|
|
1038
|
+
resize the bitmap
|
|
1039
|
+
"""
|
|
1040
|
+
|
|
1041
|
+
if self.polygonsList2:
|
|
1042
|
+
if (
|
|
1043
|
+
dialog.MessageDialog(
|
|
1044
|
+
"BORIS - Behaviors map creator",
|
|
1045
|
+
"The map contains modifiers. Erase all modifiers",
|
|
1046
|
+
(cfg.NO, cfg.YES),
|
|
1047
|
+
)
|
|
1048
|
+
== cfg.NO
|
|
1049
|
+
):
|
|
1050
|
+
return
|
|
1051
|
+
|
|
1052
|
+
integer, ok = QInputDialog.getInt(
|
|
1053
|
+
self, "Resize image", "New horizontal size (in pixels)", value=self.pixmap.size().width(), minValue=100, maxValue=3048, step=10
|
|
1054
|
+
)
|
|
1055
|
+
if not ok:
|
|
1056
|
+
return
|
|
1057
|
+
|
|
1058
|
+
self.cancelMap()
|
|
1059
|
+
|
|
1060
|
+
h = int(self.pixmap.size().width() / (self.pixmap.size().width() / integer))
|
|
1061
|
+
|
|
1062
|
+
self.pixmap = self.pixmap.scaled(integer, h, Qt.KeepAspectRatio)
|
|
1063
|
+
|
|
1064
|
+
self.view.scene().clear()
|
|
1065
|
+
self.view.setSceneRect(0, 0, self.pixmap.size().width(), self.pixmap.size().height())
|
|
1066
|
+
pixitem = QGraphicsPixmapItem(self.pixmap)
|
|
1067
|
+
pixitem.setPos(0, 0)
|
|
1068
|
+
self.view.scene().addItem(pixitem)
|
|
1069
|
+
|
|
1070
|
+
QMessageBox.information(
|
|
1071
|
+
self,
|
|
1072
|
+
cfg.programName,
|
|
1073
|
+
(
|
|
1074
|
+
f"The bitmap was resized to {self.pixmap.size().width()}x{self.pixmap.size().height()} pixels\n"
|
|
1075
|
+
"The original file was not modified"
|
|
1076
|
+
),
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def behaviors_coding_map_creator(self):
|
|
1081
|
+
"""
|
|
1082
|
+
show behaviors coding map creator window
|
|
1083
|
+
"""
|
|
1084
|
+
|
|
1085
|
+
if not self.project:
|
|
1086
|
+
QMessageBox.warning(
|
|
1087
|
+
self, cfg.programName, "Create or open a project first", QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton
|
|
1088
|
+
)
|
|
1089
|
+
return
|
|
1090
|
+
|
|
1091
|
+
codes_list = [self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] for key in self.pj[cfg.ETHOGRAM]]
|
|
1092
|
+
|
|
1093
|
+
self.mapCreatorWindow = BehaviorsMapCreatorWindow(codes_list)
|
|
1094
|
+
# behaviors coding map list
|
|
1095
|
+
self.mapCreatorWindow.bcm_list = [x["name"].upper() for x in self.pj.get(cfg.BEHAVIORS_CODING_MAP, [])]
|
|
1096
|
+
self.mapCreatorWindow.signal_add_to_project.connect(self.behaviors_coding_map_creator_signal_addtoproject)
|
|
1097
|
+
self.mapCreatorWindow.move(self.pos())
|
|
1098
|
+
self.mapCreatorWindow.resize(cfg.CODING_MAP_RESIZE_W, cfg.CODING_MAP_RESIZE_H)
|
|
1099
|
+
self.mapCreatorWindow.show()
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
if __name__ == "__main__":
|
|
1103
|
+
import sys
|
|
1104
|
+
|
|
1105
|
+
app = QApplication(sys.argv)
|
|
1106
|
+
window = BehaviorsMapCreatorWindow(["North zone", "East zone", "South zone", "West zone"])
|
|
1107
|
+
window.bcm_list = []
|
|
1108
|
+
gui_utilities.resize_center(app, window, cfg.CODING_MAP_RESIZE_W, cfg.CODING_MAP_RESIZE_H)
|
|
1109
|
+
window.show()
|
|
1110
|
+
sys.exit(app.exec())
|