unitrefine 0.2.0__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.
- unitrefine/__init__.py +75 -0
- unitrefine/apply_models.py +111 -0
- unitrefine/cli.py +251 -0
- unitrefine/evaluation.py +457 -0
- unitrefine/figures.py +1458 -0
- unitrefine/import_labels.py +176 -0
- unitrefine/launch_sigui.py +138 -0
- unitrefine/launch_sigui_validate.py +243 -0
- unitrefine/launcher.py +239 -0
- unitrefine/main.py +2395 -0
- unitrefine/manage_models.py +193 -0
- unitrefine/metrics.py +146 -0
- unitrefine/model_selection.py +159 -0
- unitrefine/models_hub.py +118 -0
- unitrefine/phy_curation.py +306 -0
- unitrefine/phy_import.py +428 -0
- unitrefine/predictions.py +507 -0
- unitrefine/resources/curation_gui.jpg +0 -0
- unitrefine/resources/fonts/Inter-Bold.ttf +0 -0
- unitrefine/resources/fonts/Inter-LICENSE.txt +92 -0
- unitrefine/resources/fonts/Inter-Regular.ttf +0 -0
- unitrefine/resources/logo.png +0 -0
- unitrefine/resources/model_training_example.jpg +0 -0
- unitrefine/resources/predict-gui.JPG +0 -0
- unitrefine/resources/re-label.JPG +0 -0
- unitrefine/resources/si-gui.JPG +0 -0
- unitrefine/resources/train_and_load_gui.JPG +0 -0
- unitrefine/resources/unit_types_example.jpg +0 -0
- unitrefine/resources/unitrefine_gui.JPG +0 -0
- unitrefine/retraining_history.py +220 -0
- unitrefine/sigui_progress.py +157 -0
- unitrefine/theme.py +254 -0
- unitrefine/train.py +622 -0
- unitrefine/widgets.py +961 -0
- unitrefine-0.2.0.dist-info/METADATA +153 -0
- unitrefine-0.2.0.dist-info/RECORD +40 -0
- unitrefine-0.2.0.dist-info/WHEEL +5 -0
- unitrefine-0.2.0.dist-info/entry_points.txt +4 -0
- unitrefine-0.2.0.dist-info/licenses/LICENSE +21 -0
- unitrefine-0.2.0.dist-info/top_level.txt +1 -0
unitrefine/launcher.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Starts UnitRefine. An "Opening UnitRefine" box appears at once, while the slow parts run behind it: loading
|
|
3
|
+
SpikeInterface and the machine learning libraries, loading the project, and building the window with its
|
|
4
|
+
saved figures. The window is only shown once it is complete, so it doesn't flicker while it fills in.
|
|
5
|
+
|
|
6
|
+
Only PyQt5 is imported at the top of this file, so the box can appear before the slow imports.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import importlib.util
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
from argparse import ArgumentParser
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import PyQt5.QtWidgets as QtWidgets
|
|
17
|
+
if importlib.util.find_spec("spikeinterface_gui") is None:
|
|
18
|
+
raise ImportError("No module named 'spikeinterface_gui'")
|
|
19
|
+
except ImportError as error:
|
|
20
|
+
# installed without the GUI, e.g. on a computing cluster
|
|
21
|
+
sys.exit(f"The UnitRefine GUI is not installed ({error}). Install it with: pip install \"unitrefine[gui]\" "
|
|
22
|
+
f"(or `uv sync` in the repository). Without the GUI, models can be applied with unitrefine-apply.")
|
|
23
|
+
from PyQt5.QtCore import QCoreApplication, QEvent, Qt
|
|
24
|
+
from PyQt5.QtGui import QFontDatabase, QIcon, QPixmap
|
|
25
|
+
from PyQt5.QtWidgets import QStyleFactory
|
|
26
|
+
|
|
27
|
+
from unitrefine import __version__, theme
|
|
28
|
+
|
|
29
|
+
RESOURCES_FOLDER = Path(__file__).absolute().parent / "resources"
|
|
30
|
+
|
|
31
|
+
STARTUP_STEPS = [
|
|
32
|
+
"Loading SpikeInterface and the machine learning libraries",
|
|
33
|
+
"Loading the project",
|
|
34
|
+
"Building the window and loading the saved figures",
|
|
35
|
+
]
|
|
36
|
+
# for a new project, after loading it
|
|
37
|
+
DOWNLOAD_STEP = "Downloading the pretrained models (about 30 MB)"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def enable_high_dpi_scaling():
|
|
41
|
+
"""
|
|
42
|
+
Makes the GUI follow the display scaling set in the operating system (e.g. 150% on a laptop with a
|
|
43
|
+
high resolution screen), so it has the same physical size on every screen. Must be called before
|
|
44
|
+
the QApplication is created.
|
|
45
|
+
"""
|
|
46
|
+
QtWidgets.QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
|
|
47
|
+
QtWidgets.QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
|
48
|
+
# use the exact scaling (e.g. 1.25) instead of rounding it to a whole number
|
|
49
|
+
QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def set_app_font(app):
|
|
53
|
+
"""
|
|
54
|
+
Uses Inter, which is included in resources/fonts so the GUI looks the same on every system.
|
|
55
|
+
If it can't be loaded, falls back to Segoe UI (Windows) or the system's default font.
|
|
56
|
+
"""
|
|
57
|
+
loaded_families = set()
|
|
58
|
+
for font_file in (RESOURCES_FOLDER / "fonts").glob("*.ttf"):
|
|
59
|
+
font_id = QFontDatabase.addApplicationFont(str(font_file))
|
|
60
|
+
if font_id != -1:
|
|
61
|
+
loaded_families.update(QFontDatabase.applicationFontFamilies(font_id))
|
|
62
|
+
|
|
63
|
+
custom_font = app.font()
|
|
64
|
+
if "Inter" in loaded_families:
|
|
65
|
+
custom_font.setFamily("Inter")
|
|
66
|
+
elif "Segoe UI" in QFontDatabase().families():
|
|
67
|
+
custom_font.setFamily("Segoe UI")
|
|
68
|
+
custom_font.setPointSize(10)
|
|
69
|
+
app.setFont(custom_font)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class StartupDialog(QtWidgets.QWidget):
|
|
73
|
+
"""The 'Opening UnitRefine' box: the steps of opening, which is done, and the time so far."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, project_folder, steps=STARTUP_STEPS):
|
|
76
|
+
super().__init__(None, Qt.SplashScreen | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
|
77
|
+
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
|
78
|
+
self.start_time = time.time()
|
|
79
|
+
self.current_step = -1
|
|
80
|
+
self.steps = steps
|
|
81
|
+
|
|
82
|
+
outer_layout = QtWidgets.QVBoxLayout(self)
|
|
83
|
+
outer_layout.setContentsMargins(0, 0, 0, 0)
|
|
84
|
+
card = QtWidgets.QFrame()
|
|
85
|
+
card.setObjectName("startup_card")
|
|
86
|
+
card.setStyleSheet(
|
|
87
|
+
f"QFrame#startup_card {{ background-color: white; border: 1px solid {theme.BORDER}; border-radius: 14px; }}"
|
|
88
|
+
)
|
|
89
|
+
outer_layout.addWidget(card)
|
|
90
|
+
layout = QtWidgets.QVBoxLayout(card)
|
|
91
|
+
layout.setContentsMargins(28, 24, 28, 22)
|
|
92
|
+
layout.setSpacing(10)
|
|
93
|
+
|
|
94
|
+
header_layout = QtWidgets.QHBoxLayout()
|
|
95
|
+
logo_file = RESOURCES_FOLDER / "logo.png"
|
|
96
|
+
if logo_file.exists():
|
|
97
|
+
logo = QtWidgets.QLabel()
|
|
98
|
+
logo.setPixmap(QPixmap(str(logo_file)).scaled(44, 44, Qt.KeepAspectRatio, Qt.SmoothTransformation))
|
|
99
|
+
header_layout.addWidget(logo)
|
|
100
|
+
title_layout = QtWidgets.QVBoxLayout()
|
|
101
|
+
title_layout.setSpacing(0)
|
|
102
|
+
title = QtWidgets.QLabel("Opening UnitRefine")
|
|
103
|
+
title.setStyleSheet("font-weight: bold; font-size: 15pt;")
|
|
104
|
+
title_layout.addWidget(title)
|
|
105
|
+
project_label = QtWidgets.QLabel(f"Project: {Path(project_folder).name}")
|
|
106
|
+
project_label.setObjectName("muted")
|
|
107
|
+
title_layout.addWidget(project_label)
|
|
108
|
+
header_layout.addLayout(title_layout, 1)
|
|
109
|
+
layout.addLayout(header_layout)
|
|
110
|
+
|
|
111
|
+
self.step_labels = []
|
|
112
|
+
for step_text in self.steps:
|
|
113
|
+
step_label = QtWidgets.QLabel()
|
|
114
|
+
step_label.setTextFormat(Qt.TextFormat.RichText)
|
|
115
|
+
layout.addWidget(step_label)
|
|
116
|
+
self.step_labels.append(step_label)
|
|
117
|
+
|
|
118
|
+
self.progress_bar = QtWidgets.QProgressBar()
|
|
119
|
+
self.progress_bar.setRange(0, len(self.steps))
|
|
120
|
+
self.progress_bar.setTextVisible(False)
|
|
121
|
+
self.progress_bar.setFixedHeight(8)
|
|
122
|
+
layout.addWidget(self.progress_bar)
|
|
123
|
+
|
|
124
|
+
self.time_label = QtWidgets.QLabel()
|
|
125
|
+
self.time_label.setObjectName("muted")
|
|
126
|
+
layout.addWidget(self.time_label)
|
|
127
|
+
|
|
128
|
+
self.setFixedWidth(500)
|
|
129
|
+
self.update_display()
|
|
130
|
+
|
|
131
|
+
def set_step(self, step_index):
|
|
132
|
+
self.current_step = step_index
|
|
133
|
+
self.update_display()
|
|
134
|
+
|
|
135
|
+
def update_display(self):
|
|
136
|
+
for index, (step_text, step_label) in enumerate(zip(self.steps, self.step_labels)):
|
|
137
|
+
if index < self.current_step:
|
|
138
|
+
step_label.setText(f"<span style='color:{theme.SUCCESS}'>✓</span> {step_text}")
|
|
139
|
+
step_label.setStyleSheet(f"color: {theme.MUTED};")
|
|
140
|
+
elif index == self.current_step:
|
|
141
|
+
step_label.setText(f"<span style='color:{theme.ACCENT}'>●</span> <b>{step_text}…</b>")
|
|
142
|
+
step_label.setStyleSheet("")
|
|
143
|
+
else:
|
|
144
|
+
step_label.setText(f"○ {step_text}")
|
|
145
|
+
step_label.setStyleSheet("color: #98A2B3;")
|
|
146
|
+
self.progress_bar.setValue(max(self.current_step, 0))
|
|
147
|
+
self.time_label.setText(f"{time.time() - self.start_time:.0f} s · the first start after installing can take a minute")
|
|
148
|
+
# draw now: the slow steps run in this thread, so the box can't redraw itself while they run
|
|
149
|
+
for _ in range(3):
|
|
150
|
+
QtWidgets.QApplication.processEvents()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def main():
|
|
154
|
+
|
|
155
|
+
parser = ArgumentParser(
|
|
156
|
+
description="UnitRefine - curate your sorting and create a machine learning model based on your curation."
|
|
157
|
+
)
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"--project_folder",
|
|
160
|
+
required=True,
|
|
161
|
+
type=str,
|
|
162
|
+
)
|
|
163
|
+
parser.add_argument("--no_pretrained_models", action="store_true",
|
|
164
|
+
help="don't download the pretrained models into a new project (they can be added later "
|
|
165
|
+
"with + Load from HFH)")
|
|
166
|
+
parser.add_argument("--version", action="version", version=f"unitrefine {__version__}")
|
|
167
|
+
args = parser.parse_args()
|
|
168
|
+
project_folder = Path(args.project_folder).resolve()
|
|
169
|
+
new_project = not project_folder.is_dir()
|
|
170
|
+
# a new project comes with the pretrained models, so they can be applied straight away
|
|
171
|
+
download_models = new_project and not args.no_pretrained_models
|
|
172
|
+
steps = STARTUP_STEPS[:2] + ([DOWNLOAD_STEP] if download_models else []) + STARTUP_STEPS[2:]
|
|
173
|
+
|
|
174
|
+
enable_high_dpi_scaling()
|
|
175
|
+
app = QtWidgets.QApplication(sys.argv)
|
|
176
|
+
app.setStyle(QStyleFactory.create("Fusion"))
|
|
177
|
+
app.setStyleSheet(theme.STYLE_SHEET)
|
|
178
|
+
icon_file = RESOURCES_FOLDER / "logo.png"
|
|
179
|
+
if icon_file.exists():
|
|
180
|
+
app.setWindowIcon(QIcon(str(icon_file)))
|
|
181
|
+
set_app_font(app)
|
|
182
|
+
|
|
183
|
+
startup = StartupDialog(project_folder, steps)
|
|
184
|
+
startup.show()
|
|
185
|
+
startup.set_step(0)
|
|
186
|
+
|
|
187
|
+
from unitrefine.main import APP_DESCRIPTION, MainWindow, UnitRefineProject, initial_window_size, load_project
|
|
188
|
+
|
|
189
|
+
startup.set_step(1)
|
|
190
|
+
if not new_project:
|
|
191
|
+
print("Project already exists. Loading...")
|
|
192
|
+
project = load_project(project_folder)
|
|
193
|
+
else:
|
|
194
|
+
print("Project Folder does not exist. Creating now...")
|
|
195
|
+
project = UnitRefineProject(project_folder)
|
|
196
|
+
project.save()
|
|
197
|
+
|
|
198
|
+
not_downloaded = {}
|
|
199
|
+
if download_models:
|
|
200
|
+
from unitrefine.models_hub import download_default_models
|
|
201
|
+
|
|
202
|
+
startup.set_step(2)
|
|
203
|
+
print("Downloading the pretrained models...")
|
|
204
|
+
not_downloaded = download_default_models(project_folder / "models")
|
|
205
|
+
# load it again, with the models
|
|
206
|
+
project = load_project(project_folder)
|
|
207
|
+
|
|
208
|
+
startup.set_step(len(steps) - 1)
|
|
209
|
+
window = MainWindow(project)
|
|
210
|
+
window.setWindowTitle(f"UnitRefine: {APP_DESCRIPTION}")
|
|
211
|
+
window.resize(initial_window_size(app.primaryScreen().availableGeometry()))
|
|
212
|
+
if download_models:
|
|
213
|
+
choose_pretrained_cascade(window)
|
|
214
|
+
window.finish_loading()
|
|
215
|
+
|
|
216
|
+
startup.set_step(len(steps))
|
|
217
|
+
window.show()
|
|
218
|
+
QtWidgets.QApplication.processEvents()
|
|
219
|
+
startup.close()
|
|
220
|
+
if len(not_downloaded) > 0:
|
|
221
|
+
QtWidgets.QMessageBox.information(
|
|
222
|
+
window, "Pretrained models not downloaded",
|
|
223
|
+
"These pretrained models could not be downloaded (is the computer connected to the internet?):\n"
|
|
224
|
+
+ "\n".join(f"• {repo_id}" for repo_id in not_downloaded)
|
|
225
|
+
+ "\n\nYou can add them later with + Load from HFH in section 3.1.")
|
|
226
|
+
app.exec()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def choose_pretrained_cascade(window):
|
|
230
|
+
"""In a new project, chooses the pretrained noise vs neural and SUA vs MUA models as a cascade (section 3.2)."""
|
|
231
|
+
selector = window.model_selector
|
|
232
|
+
selector.cascade_radio.setChecked(True)
|
|
233
|
+
if selector.describe_problem() is not None:
|
|
234
|
+
# e.g. one of them could not be downloaded
|
|
235
|
+
selector.binary_radio.setChecked(True)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
main()
|