celldetective 1.5.0b11__py3-none-any.whl → 1.5.0b13__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.
- celldetective/_version.py +1 -1
- celldetective/gui/tableUI.py +2 -3
- celldetective/utils/parsing.py +5 -0
- {celldetective-1.5.0b11.dist-info → celldetective-1.5.0b13.dist-info}/METADATA +1 -1
- {celldetective-1.5.0b11.dist-info → celldetective-1.5.0b13.dist-info}/RECORD +12 -9
- tests/gui/test_classifier_widget.py +754 -0
- tests/gui/test_measurement_settings.py +1083 -0
- tests/gui/test_tableui_track_collapse.py +239 -0
- {celldetective-1.5.0b11.dist-info → celldetective-1.5.0b13.dist-info}/WHEEL +0 -0
- {celldetective-1.5.0b11.dist-info → celldetective-1.5.0b13.dist-info}/entry_points.txt +0 -0
- {celldetective-1.5.0b11.dist-info → celldetective-1.5.0b13.dist-info}/licenses/LICENSE +0 -0
- {celldetective-1.5.0b11.dist-info → celldetective-1.5.0b13.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1083 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Measurement Settings UI.
|
|
3
|
+
Tests various combinations of measurement settings to ensure they can be set and run without bugs.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import os
|
|
8
|
+
import numpy as np
|
|
9
|
+
import logging
|
|
10
|
+
import json
|
|
11
|
+
from PyQt5 import QtCore
|
|
12
|
+
import tifffile
|
|
13
|
+
|
|
14
|
+
from celldetective.gui.InitWindow import AppInitWindow
|
|
15
|
+
from celldetective.gui.settings._settings_measurements import SettingsMeasurements
|
|
16
|
+
from celldetective import get_software_location
|
|
17
|
+
from unittest.mock import patch
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
software_location = get_software_location()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture(autouse=True)
|
|
24
|
+
def disable_logging():
|
|
25
|
+
"""Disable all logging to avoid Windows OSError with pytest capture."""
|
|
26
|
+
logger = logging.getLogger()
|
|
27
|
+
try:
|
|
28
|
+
logging.disable(logging.CRITICAL)
|
|
29
|
+
yield
|
|
30
|
+
finally:
|
|
31
|
+
logging.disable(logging.NOTSET)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def app(qtbot):
|
|
36
|
+
test_app = AppInitWindow(software_location=software_location)
|
|
37
|
+
qtbot.addWidget(test_app)
|
|
38
|
+
return test_app
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def create_dummy_movie(
|
|
42
|
+
exp_dir, well="W1", pos="100", prefix="sample", frames=5, channels=2
|
|
43
|
+
):
|
|
44
|
+
"""Create a dummy movie with multiple channels."""
|
|
45
|
+
movie_dir = os.path.join(exp_dir, well, pos, "movie")
|
|
46
|
+
os.makedirs(movie_dir, exist_ok=True)
|
|
47
|
+
movie_path = os.path.join(movie_dir, f"{prefix}.tif")
|
|
48
|
+
# Create multi-channel, multi-frame movie
|
|
49
|
+
img = np.zeros((frames * channels, 100, 100), dtype=np.uint16)
|
|
50
|
+
tifffile.imwrite(movie_path, img)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def setup_experiment_dir(tmp_path, well="W1", pos="100"):
|
|
54
|
+
"""Set up a complete experiment directory structure."""
|
|
55
|
+
exp_dir = str(tmp_path / "Experiment")
|
|
56
|
+
os.makedirs(os.path.join(exp_dir, well, pos, "output", "tables"), exist_ok=True)
|
|
57
|
+
os.makedirs(os.path.join(exp_dir, well, pos, "labels_targets"), exist_ok=True)
|
|
58
|
+
os.makedirs(os.path.join(exp_dir, "configs"), exist_ok=True)
|
|
59
|
+
|
|
60
|
+
with open(os.path.join(exp_dir, "config.ini"), "w") as f:
|
|
61
|
+
f.write(
|
|
62
|
+
"[MovieSettings]\nmovie_prefix = sample\nlen_movie = 10\nshape_x = 100\nshape_y = 100\npxtoum = 1.0\nframetomin = 1.0\n"
|
|
63
|
+
)
|
|
64
|
+
f.write(
|
|
65
|
+
"[Labels]\nconcentrations = 0\ncell_types = dummy\nantibodies = none\npharmaceutical_agents = none\n"
|
|
66
|
+
)
|
|
67
|
+
f.write("[Channels]\nDAPI = 0\nGFP = 1\n")
|
|
68
|
+
|
|
69
|
+
create_dummy_movie(
|
|
70
|
+
exp_dir, well=well, pos=pos, prefix="sample", frames=10, channels=2
|
|
71
|
+
)
|
|
72
|
+
return exp_dir
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TestMeasurementSettingsUI:
|
|
76
|
+
"""Tests for SettingsMeasurements UI instantiation and basic functionality."""
|
|
77
|
+
|
|
78
|
+
def test_open_settings_panel(self, app, qtbot, tmp_path):
|
|
79
|
+
"""Test that the measurement settings panel can be opened."""
|
|
80
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
81
|
+
|
|
82
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
83
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
84
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
85
|
+
|
|
86
|
+
cp = app.control_panel
|
|
87
|
+
p0 = cp.ProcessPopulations[0]
|
|
88
|
+
|
|
89
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
90
|
+
|
|
91
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
92
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
93
|
+
cp.update_position_options()
|
|
94
|
+
qtbot.wait(500)
|
|
95
|
+
|
|
96
|
+
# Click to open measurement settings
|
|
97
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
qtbot.waitUntil(
|
|
101
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
102
|
+
)
|
|
103
|
+
except Exception:
|
|
104
|
+
pytest.skip("settings_measurements not available on p0")
|
|
105
|
+
|
|
106
|
+
settings = p0.settings_measurements
|
|
107
|
+
assert settings is not None
|
|
108
|
+
assert isinstance(settings, SettingsMeasurements)
|
|
109
|
+
|
|
110
|
+
settings.close()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TestFeatureSettings:
|
|
114
|
+
"""Tests for feature list configuration."""
|
|
115
|
+
|
|
116
|
+
def test_features_list_exists(self, app, qtbot, tmp_path):
|
|
117
|
+
"""Test that features list widget exists and is populated."""
|
|
118
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
119
|
+
|
|
120
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
121
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
122
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
123
|
+
|
|
124
|
+
cp = app.control_panel
|
|
125
|
+
p0 = cp.ProcessPopulations[0]
|
|
126
|
+
|
|
127
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
128
|
+
|
|
129
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
130
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
131
|
+
cp.update_position_options()
|
|
132
|
+
qtbot.wait(500)
|
|
133
|
+
|
|
134
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
qtbot.waitUntil(
|
|
138
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
139
|
+
)
|
|
140
|
+
except Exception:
|
|
141
|
+
pytest.skip("settings_measurements not available")
|
|
142
|
+
|
|
143
|
+
settings = p0.settings_measurements
|
|
144
|
+
assert hasattr(settings, "features_list")
|
|
145
|
+
assert settings.features_list is not None
|
|
146
|
+
|
|
147
|
+
# Default features should be populated
|
|
148
|
+
items = settings.features_list.getItems()
|
|
149
|
+
assert "area" in items or len(items) >= 0
|
|
150
|
+
|
|
151
|
+
settings.close()
|
|
152
|
+
|
|
153
|
+
def test_add_feature(self, app, qtbot, tmp_path):
|
|
154
|
+
"""Test adding a feature to the list."""
|
|
155
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
156
|
+
|
|
157
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
158
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
159
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
160
|
+
|
|
161
|
+
cp = app.control_panel
|
|
162
|
+
p0 = cp.ProcessPopulations[0]
|
|
163
|
+
|
|
164
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
165
|
+
|
|
166
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
167
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
168
|
+
cp.update_position_options()
|
|
169
|
+
qtbot.wait(500)
|
|
170
|
+
|
|
171
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
qtbot.waitUntil(
|
|
175
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
176
|
+
)
|
|
177
|
+
except Exception:
|
|
178
|
+
pytest.skip("settings_measurements not available")
|
|
179
|
+
|
|
180
|
+
settings = p0.settings_measurements
|
|
181
|
+
|
|
182
|
+
initial_count = settings.features_list.list_widget.count()
|
|
183
|
+
settings.add_feature_btn.click()
|
|
184
|
+
qtbot.wait(100)
|
|
185
|
+
|
|
186
|
+
new_count = settings.features_list.list_widget.count()
|
|
187
|
+
assert new_count >= initial_count
|
|
188
|
+
|
|
189
|
+
settings.close()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class TestContourSettings:
|
|
193
|
+
"""Tests for contour measurement (border distance) settings."""
|
|
194
|
+
|
|
195
|
+
def test_contours_list_exists(self, app, qtbot, tmp_path):
|
|
196
|
+
"""Test that contours list widget exists."""
|
|
197
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
198
|
+
|
|
199
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
200
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
201
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
202
|
+
|
|
203
|
+
cp = app.control_panel
|
|
204
|
+
p0 = cp.ProcessPopulations[0]
|
|
205
|
+
|
|
206
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
207
|
+
|
|
208
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
209
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
210
|
+
cp.update_position_options()
|
|
211
|
+
qtbot.wait(500)
|
|
212
|
+
|
|
213
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
qtbot.waitUntil(
|
|
217
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
218
|
+
)
|
|
219
|
+
except Exception:
|
|
220
|
+
pytest.skip("settings_measurements not available")
|
|
221
|
+
|
|
222
|
+
settings = p0.settings_measurements
|
|
223
|
+
assert hasattr(settings, "contours_list")
|
|
224
|
+
assert settings.contours_list is not None
|
|
225
|
+
|
|
226
|
+
settings.close()
|
|
227
|
+
|
|
228
|
+
def test_add_contour_distance(self, app, qtbot, tmp_path):
|
|
229
|
+
"""Test adding a contour distance."""
|
|
230
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
231
|
+
|
|
232
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
233
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
234
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
235
|
+
|
|
236
|
+
cp = app.control_panel
|
|
237
|
+
p0 = cp.ProcessPopulations[0]
|
|
238
|
+
|
|
239
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
240
|
+
|
|
241
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
242
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
243
|
+
cp.update_position_options()
|
|
244
|
+
qtbot.wait(500)
|
|
245
|
+
|
|
246
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
qtbot.waitUntil(
|
|
250
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
251
|
+
)
|
|
252
|
+
except Exception:
|
|
253
|
+
pytest.skip("settings_measurements not available")
|
|
254
|
+
|
|
255
|
+
settings = p0.settings_measurements
|
|
256
|
+
|
|
257
|
+
initial_count = settings.contours_list.list_widget.count()
|
|
258
|
+
settings.add_contour_btn.click()
|
|
259
|
+
qtbot.wait(100)
|
|
260
|
+
|
|
261
|
+
new_count = settings.contours_list.list_widget.count()
|
|
262
|
+
assert new_count >= initial_count
|
|
263
|
+
|
|
264
|
+
settings.close()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class TestHaralickTextureSettings:
|
|
268
|
+
"""Tests for Haralick texture measurement settings."""
|
|
269
|
+
|
|
270
|
+
def test_haralick_checkbox_exists(self, app, qtbot, tmp_path):
|
|
271
|
+
"""Test that Haralick checkbox exists."""
|
|
272
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
273
|
+
|
|
274
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
275
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
276
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
277
|
+
|
|
278
|
+
cp = app.control_panel
|
|
279
|
+
p0 = cp.ProcessPopulations[0]
|
|
280
|
+
|
|
281
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
282
|
+
|
|
283
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
284
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
285
|
+
cp.update_position_options()
|
|
286
|
+
qtbot.wait(500)
|
|
287
|
+
|
|
288
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
qtbot.waitUntil(
|
|
292
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
293
|
+
)
|
|
294
|
+
except Exception:
|
|
295
|
+
pytest.skip("settings_measurements not available")
|
|
296
|
+
|
|
297
|
+
settings = p0.settings_measurements
|
|
298
|
+
assert hasattr(settings, "activate_haralick_btn")
|
|
299
|
+
assert not settings.activate_haralick_btn.isChecked()
|
|
300
|
+
|
|
301
|
+
settings.close()
|
|
302
|
+
|
|
303
|
+
def test_enable_haralick_shows_options(self, app, qtbot, tmp_path):
|
|
304
|
+
"""Test that enabling Haralick shows additional options."""
|
|
305
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
306
|
+
|
|
307
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
308
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
309
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
310
|
+
|
|
311
|
+
cp = app.control_panel
|
|
312
|
+
p0 = cp.ProcessPopulations[0]
|
|
313
|
+
|
|
314
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
315
|
+
|
|
316
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
317
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
318
|
+
cp.update_position_options()
|
|
319
|
+
qtbot.wait(500)
|
|
320
|
+
|
|
321
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
qtbot.waitUntil(
|
|
325
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
326
|
+
)
|
|
327
|
+
except Exception:
|
|
328
|
+
pytest.skip("settings_measurements not available")
|
|
329
|
+
|
|
330
|
+
settings = p0.settings_measurements
|
|
331
|
+
|
|
332
|
+
# Initially Haralick options should be disabled
|
|
333
|
+
assert not settings.haralick_channel_choice.isEnabled()
|
|
334
|
+
assert not settings.haralick_distance_le.isEnabled()
|
|
335
|
+
|
|
336
|
+
# Enable Haralick
|
|
337
|
+
settings.activate_haralick_btn.setChecked(True)
|
|
338
|
+
qtbot.wait(100)
|
|
339
|
+
|
|
340
|
+
# Now options should be enabled
|
|
341
|
+
assert settings.haralick_channel_choice.isEnabled()
|
|
342
|
+
assert settings.haralick_distance_le.isEnabled()
|
|
343
|
+
assert settings.haralick_n_gray_levels_le.isEnabled()
|
|
344
|
+
|
|
345
|
+
settings.close()
|
|
346
|
+
|
|
347
|
+
def test_haralick_channel_selection(self, app, qtbot, tmp_path):
|
|
348
|
+
"""Test Haralick channel selection."""
|
|
349
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
350
|
+
|
|
351
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
352
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
353
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
354
|
+
|
|
355
|
+
cp = app.control_panel
|
|
356
|
+
p0 = cp.ProcessPopulations[0]
|
|
357
|
+
|
|
358
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
359
|
+
|
|
360
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
361
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
362
|
+
cp.update_position_options()
|
|
363
|
+
qtbot.wait(500)
|
|
364
|
+
|
|
365
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
qtbot.waitUntil(
|
|
369
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
370
|
+
)
|
|
371
|
+
except Exception:
|
|
372
|
+
pytest.skip("settings_measurements not available")
|
|
373
|
+
|
|
374
|
+
settings = p0.settings_measurements
|
|
375
|
+
settings.activate_haralick_btn.setChecked(True)
|
|
376
|
+
qtbot.wait(100)
|
|
377
|
+
|
|
378
|
+
# Should have channel options
|
|
379
|
+
assert settings.haralick_channel_choice.count() >= 1
|
|
380
|
+
|
|
381
|
+
# Change channel
|
|
382
|
+
if settings.haralick_channel_choice.count() > 1:
|
|
383
|
+
settings.haralick_channel_choice.setCurrentIndex(1)
|
|
384
|
+
qtbot.wait(50)
|
|
385
|
+
assert settings.haralick_channel_choice.currentIndex() == 1
|
|
386
|
+
|
|
387
|
+
settings.close()
|
|
388
|
+
|
|
389
|
+
def test_haralick_normalization_mode_toggle(self, app, qtbot, tmp_path):
|
|
390
|
+
"""Test Haralick normalization mode toggle between percentile and absolute."""
|
|
391
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
392
|
+
|
|
393
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
394
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
395
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
396
|
+
|
|
397
|
+
cp = app.control_panel
|
|
398
|
+
p0 = cp.ProcessPopulations[0]
|
|
399
|
+
|
|
400
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
401
|
+
|
|
402
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
403
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
404
|
+
cp.update_position_options()
|
|
405
|
+
qtbot.wait(500)
|
|
406
|
+
|
|
407
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
qtbot.waitUntil(
|
|
411
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
412
|
+
)
|
|
413
|
+
except Exception:
|
|
414
|
+
pytest.skip("settings_measurements not available")
|
|
415
|
+
|
|
416
|
+
settings = p0.settings_measurements
|
|
417
|
+
settings.activate_haralick_btn.setChecked(True)
|
|
418
|
+
qtbot.wait(100)
|
|
419
|
+
|
|
420
|
+
# Initially in percentile mode
|
|
421
|
+
assert settings.percentile_mode is True
|
|
422
|
+
assert (
|
|
423
|
+
"percentile" in settings.haralick_percentile_min_lbl.text().lower()
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Toggle to absolute mode
|
|
427
|
+
settings.haralick_normalization_mode_btn.click()
|
|
428
|
+
qtbot.wait(100)
|
|
429
|
+
|
|
430
|
+
assert settings.percentile_mode is False
|
|
431
|
+
assert "value" in settings.haralick_percentile_min_lbl.text().lower()
|
|
432
|
+
|
|
433
|
+
settings.close()
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class TestIsotropicMeasurementSettings:
|
|
437
|
+
"""Tests for isotropic (radii and operations) measurement settings."""
|
|
438
|
+
|
|
439
|
+
def test_radii_list_exists(self, app, qtbot, tmp_path):
|
|
440
|
+
"""Test that radii list widget exists."""
|
|
441
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
442
|
+
|
|
443
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
444
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
445
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
446
|
+
|
|
447
|
+
cp = app.control_panel
|
|
448
|
+
p0 = cp.ProcessPopulations[0]
|
|
449
|
+
|
|
450
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
451
|
+
|
|
452
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
453
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
454
|
+
cp.update_position_options()
|
|
455
|
+
qtbot.wait(500)
|
|
456
|
+
|
|
457
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
qtbot.waitUntil(
|
|
461
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
462
|
+
)
|
|
463
|
+
except Exception:
|
|
464
|
+
pytest.skip("settings_measurements not available")
|
|
465
|
+
|
|
466
|
+
settings = p0.settings_measurements
|
|
467
|
+
assert hasattr(settings, "radii_list")
|
|
468
|
+
assert settings.radii_list is not None
|
|
469
|
+
|
|
470
|
+
# Default should have at least one radius
|
|
471
|
+
items = settings.radii_list.getItems()
|
|
472
|
+
assert len(items) >= 0
|
|
473
|
+
|
|
474
|
+
settings.close()
|
|
475
|
+
|
|
476
|
+
def test_add_radius(self, app, qtbot, tmp_path):
|
|
477
|
+
"""Test adding a radius."""
|
|
478
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
479
|
+
|
|
480
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
481
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
482
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
483
|
+
|
|
484
|
+
cp = app.control_panel
|
|
485
|
+
p0 = cp.ProcessPopulations[0]
|
|
486
|
+
|
|
487
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
488
|
+
|
|
489
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
490
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
491
|
+
cp.update_position_options()
|
|
492
|
+
qtbot.wait(500)
|
|
493
|
+
|
|
494
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
495
|
+
|
|
496
|
+
try:
|
|
497
|
+
qtbot.waitUntil(
|
|
498
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
499
|
+
)
|
|
500
|
+
except Exception:
|
|
501
|
+
pytest.skip("settings_measurements not available")
|
|
502
|
+
|
|
503
|
+
settings = p0.settings_measurements
|
|
504
|
+
|
|
505
|
+
initial_count = settings.radii_list.list_widget.count()
|
|
506
|
+
settings.add_radius_btn.click()
|
|
507
|
+
qtbot.wait(100)
|
|
508
|
+
|
|
509
|
+
new_count = settings.radii_list.list_widget.count()
|
|
510
|
+
assert new_count >= initial_count
|
|
511
|
+
|
|
512
|
+
settings.close()
|
|
513
|
+
|
|
514
|
+
def test_operations_list_exists(self, app, qtbot, tmp_path):
|
|
515
|
+
"""Test that operations list widget exists."""
|
|
516
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
517
|
+
|
|
518
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
519
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
520
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
521
|
+
|
|
522
|
+
cp = app.control_panel
|
|
523
|
+
p0 = cp.ProcessPopulations[0]
|
|
524
|
+
|
|
525
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
526
|
+
|
|
527
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
528
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
529
|
+
cp.update_position_options()
|
|
530
|
+
qtbot.wait(500)
|
|
531
|
+
|
|
532
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
qtbot.waitUntil(
|
|
536
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
537
|
+
)
|
|
538
|
+
except Exception:
|
|
539
|
+
pytest.skip("settings_measurements not available")
|
|
540
|
+
|
|
541
|
+
settings = p0.settings_measurements
|
|
542
|
+
assert hasattr(settings, "operations_list")
|
|
543
|
+
assert settings.operations_list is not None
|
|
544
|
+
|
|
545
|
+
settings.close()
|
|
546
|
+
|
|
547
|
+
def test_add_operation(self, app, qtbot, tmp_path):
|
|
548
|
+
"""Test adding an operation."""
|
|
549
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
550
|
+
|
|
551
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
552
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
553
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
554
|
+
|
|
555
|
+
cp = app.control_panel
|
|
556
|
+
p0 = cp.ProcessPopulations[0]
|
|
557
|
+
|
|
558
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
559
|
+
|
|
560
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
561
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
562
|
+
cp.update_position_options()
|
|
563
|
+
qtbot.wait(500)
|
|
564
|
+
|
|
565
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
566
|
+
|
|
567
|
+
try:
|
|
568
|
+
qtbot.waitUntil(
|
|
569
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
570
|
+
)
|
|
571
|
+
except Exception:
|
|
572
|
+
pytest.skip("settings_measurements not available")
|
|
573
|
+
|
|
574
|
+
settings = p0.settings_measurements
|
|
575
|
+
|
|
576
|
+
initial_count = settings.operations_list.list_widget.count()
|
|
577
|
+
settings.add_op_btn.click()
|
|
578
|
+
qtbot.wait(100)
|
|
579
|
+
|
|
580
|
+
new_count = settings.operations_list.list_widget.count()
|
|
581
|
+
assert new_count >= initial_count
|
|
582
|
+
|
|
583
|
+
settings.close()
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
class TestSpotDetectionSettings:
|
|
587
|
+
"""Tests for spot detection settings."""
|
|
588
|
+
|
|
589
|
+
def test_spot_detection_checkbox_exists(self, app, qtbot, tmp_path):
|
|
590
|
+
"""Test that spot detection checkbox exists."""
|
|
591
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
592
|
+
|
|
593
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
594
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
595
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
596
|
+
|
|
597
|
+
cp = app.control_panel
|
|
598
|
+
p0 = cp.ProcessPopulations[0]
|
|
599
|
+
|
|
600
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
601
|
+
|
|
602
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
603
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
604
|
+
cp.update_position_options()
|
|
605
|
+
qtbot.wait(500)
|
|
606
|
+
|
|
607
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
608
|
+
|
|
609
|
+
try:
|
|
610
|
+
qtbot.waitUntil(
|
|
611
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
612
|
+
)
|
|
613
|
+
except Exception:
|
|
614
|
+
pytest.skip("settings_measurements not available")
|
|
615
|
+
|
|
616
|
+
settings = p0.settings_measurements
|
|
617
|
+
assert hasattr(settings, "spot_check")
|
|
618
|
+
assert not settings.spot_check.isChecked()
|
|
619
|
+
|
|
620
|
+
settings.close()
|
|
621
|
+
|
|
622
|
+
def test_enable_spot_detection(self, app, qtbot, tmp_path):
|
|
623
|
+
"""Test enabling spot detection enables related widgets."""
|
|
624
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
625
|
+
|
|
626
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
627
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
628
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
629
|
+
|
|
630
|
+
cp = app.control_panel
|
|
631
|
+
p0 = cp.ProcessPopulations[0]
|
|
632
|
+
|
|
633
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
634
|
+
|
|
635
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
636
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
637
|
+
cp.update_position_options()
|
|
638
|
+
qtbot.wait(500)
|
|
639
|
+
|
|
640
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
641
|
+
|
|
642
|
+
try:
|
|
643
|
+
qtbot.waitUntil(
|
|
644
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
645
|
+
)
|
|
646
|
+
except Exception:
|
|
647
|
+
pytest.skip("settings_measurements not available")
|
|
648
|
+
|
|
649
|
+
settings = p0.settings_measurements
|
|
650
|
+
|
|
651
|
+
# Initially spot detection widgets are disabled
|
|
652
|
+
assert not settings.spot_channel.isEnabled()
|
|
653
|
+
assert not settings.diameter_value.isEnabled()
|
|
654
|
+
|
|
655
|
+
# Enable spot detection
|
|
656
|
+
settings.spot_check.setChecked(True)
|
|
657
|
+
qtbot.wait(100)
|
|
658
|
+
|
|
659
|
+
# Now widgets should be enabled
|
|
660
|
+
assert settings.spot_channel.isEnabled()
|
|
661
|
+
assert settings.diameter_value.isEnabled()
|
|
662
|
+
assert settings.threshold_value.isEnabled()
|
|
663
|
+
|
|
664
|
+
settings.close()
|
|
665
|
+
|
|
666
|
+
def test_spot_diameter_and_threshold(self, app, qtbot, tmp_path):
|
|
667
|
+
"""Test setting spot diameter and threshold values."""
|
|
668
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
669
|
+
|
|
670
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
671
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
672
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
673
|
+
|
|
674
|
+
cp = app.control_panel
|
|
675
|
+
p0 = cp.ProcessPopulations[0]
|
|
676
|
+
|
|
677
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
678
|
+
|
|
679
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
680
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
681
|
+
cp.update_position_options()
|
|
682
|
+
qtbot.wait(500)
|
|
683
|
+
|
|
684
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
685
|
+
|
|
686
|
+
try:
|
|
687
|
+
qtbot.waitUntil(
|
|
688
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
689
|
+
)
|
|
690
|
+
except Exception:
|
|
691
|
+
pytest.skip("settings_measurements not available")
|
|
692
|
+
|
|
693
|
+
settings = p0.settings_measurements
|
|
694
|
+
settings.spot_check.setChecked(True)
|
|
695
|
+
qtbot.wait(100)
|
|
696
|
+
|
|
697
|
+
# Set diameter
|
|
698
|
+
settings.diameter_value.setText("11")
|
|
699
|
+
qtbot.wait(50)
|
|
700
|
+
assert settings.diameter_value.text() == "11"
|
|
701
|
+
|
|
702
|
+
# Set threshold
|
|
703
|
+
settings.threshold_value.setText("0.5")
|
|
704
|
+
qtbot.wait(50)
|
|
705
|
+
assert settings.threshold_value.text() == "0.5"
|
|
706
|
+
|
|
707
|
+
settings.close()
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
class TestWriteInstructions:
|
|
711
|
+
"""Tests for writing measurement instructions to file."""
|
|
712
|
+
|
|
713
|
+
def test_write_instructions_basic(self, app, qtbot, tmp_path):
|
|
714
|
+
"""Test writing basic measurement instructions."""
|
|
715
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
716
|
+
|
|
717
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
718
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
719
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
720
|
+
|
|
721
|
+
cp = app.control_panel
|
|
722
|
+
p0 = cp.ProcessPopulations[0]
|
|
723
|
+
|
|
724
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
725
|
+
|
|
726
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
727
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
728
|
+
cp.update_position_options()
|
|
729
|
+
qtbot.wait(500)
|
|
730
|
+
|
|
731
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
732
|
+
|
|
733
|
+
try:
|
|
734
|
+
qtbot.waitUntil(
|
|
735
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
736
|
+
)
|
|
737
|
+
except Exception:
|
|
738
|
+
pytest.skip("settings_measurements not available")
|
|
739
|
+
|
|
740
|
+
settings = p0.settings_measurements
|
|
741
|
+
|
|
742
|
+
# Click submit to write instructions
|
|
743
|
+
settings.submit_btn.click()
|
|
744
|
+
qtbot.wait(500)
|
|
745
|
+
|
|
746
|
+
# Check that instructions file was created
|
|
747
|
+
instructions_path = os.path.join(
|
|
748
|
+
exp_dir, "configs", f"measurement_instructions_{p0.mode}.json"
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
assert os.path.exists(instructions_path)
|
|
752
|
+
|
|
753
|
+
with open(instructions_path, "r") as f:
|
|
754
|
+
instructions = json.load(f)
|
|
755
|
+
|
|
756
|
+
assert "features" in instructions
|
|
757
|
+
assert "haralick_options" in instructions
|
|
758
|
+
|
|
759
|
+
def test_write_instructions_with_haralick(self, app, qtbot, tmp_path):
|
|
760
|
+
"""Test writing instructions with Haralick enabled."""
|
|
761
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
762
|
+
|
|
763
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
764
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
765
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
766
|
+
|
|
767
|
+
cp = app.control_panel
|
|
768
|
+
p0 = cp.ProcessPopulations[0]
|
|
769
|
+
|
|
770
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
771
|
+
|
|
772
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
773
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
774
|
+
cp.update_position_options()
|
|
775
|
+
qtbot.wait(500)
|
|
776
|
+
|
|
777
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
778
|
+
|
|
779
|
+
try:
|
|
780
|
+
qtbot.waitUntil(
|
|
781
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
782
|
+
)
|
|
783
|
+
except Exception:
|
|
784
|
+
pytest.skip("settings_measurements not available")
|
|
785
|
+
|
|
786
|
+
settings = p0.settings_measurements
|
|
787
|
+
|
|
788
|
+
# Enable Haralick with custom settings
|
|
789
|
+
settings.activate_haralick_btn.setChecked(True)
|
|
790
|
+
qtbot.wait(100)
|
|
791
|
+
|
|
792
|
+
settings.haralick_distance_le.setText("3")
|
|
793
|
+
settings.haralick_n_gray_levels_le.setText("128")
|
|
794
|
+
|
|
795
|
+
# Submit
|
|
796
|
+
settings.submit_btn.click()
|
|
797
|
+
qtbot.wait(500)
|
|
798
|
+
|
|
799
|
+
instructions_path = os.path.join(
|
|
800
|
+
exp_dir, "configs", f"measurement_instructions_{p0.mode}.json"
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
with open(instructions_path, "r") as f:
|
|
804
|
+
instructions = json.load(f)
|
|
805
|
+
|
|
806
|
+
assert instructions["haralick_options"] is not None
|
|
807
|
+
assert instructions["haralick_options"]["distance"] == 3
|
|
808
|
+
assert instructions["haralick_options"]["n_intensity_bins"] == 128
|
|
809
|
+
|
|
810
|
+
def test_write_instructions_with_spot_detection(self, app, qtbot, tmp_path):
|
|
811
|
+
"""Test writing instructions with spot detection enabled."""
|
|
812
|
+
exp_dir = setup_experiment_dir(tmp_path)
|
|
813
|
+
|
|
814
|
+
app.experiment_path_selection.setText(exp_dir)
|
|
815
|
+
qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
|
|
816
|
+
qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
|
|
817
|
+
|
|
818
|
+
cp = app.control_panel
|
|
819
|
+
p0 = cp.ProcessPopulations[0]
|
|
820
|
+
|
|
821
|
+
qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
|
|
822
|
+
|
|
823
|
+
with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
|
|
824
|
+
with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
|
|
825
|
+
cp.update_position_options()
|
|
826
|
+
qtbot.wait(500)
|
|
827
|
+
|
|
828
|
+
qtbot.mouseClick(p0.measurements_config_btn, QtCore.Qt.LeftButton)
|
|
829
|
+
|
|
830
|
+
try:
|
|
831
|
+
qtbot.waitUntil(
|
|
832
|
+
lambda: hasattr(p0, "settings_measurements"), timeout=15000
|
|
833
|
+
)
|
|
834
|
+
except Exception:
|
|
835
|
+
pytest.skip("settings_measurements not available")
|
|
836
|
+
|
|
837
|
+
settings = p0.settings_measurements
|
|
838
|
+
|
|
839
|
+
# Enable spot detection
|
|
840
|
+
settings.spot_check.setChecked(True)
|
|
841
|
+
qtbot.wait(100)
|
|
842
|
+
|
|
843
|
+
settings.diameter_value.setText("9")
|
|
844
|
+
settings.threshold_value.setText("0.3")
|
|
845
|
+
|
|
846
|
+
# Submit
|
|
847
|
+
settings.submit_btn.click()
|
|
848
|
+
qtbot.wait(500)
|
|
849
|
+
|
|
850
|
+
instructions_path = os.path.join(
|
|
851
|
+
exp_dir, "configs", f"measurement_instructions_{p0.mode}.json"
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
with open(instructions_path, "r") as f:
|
|
855
|
+
instructions = json.load(f)
|
|
856
|
+
|
|
857
|
+
assert instructions["spot_detection"] is not None
|
|
858
|
+
assert instructions["spot_detection"]["diameter"] == 9.0
|
|
859
|
+
assert instructions["spot_detection"]["threshold"] == 0.3
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
class TestMeasurementExecution:
|
|
863
|
+
"""Tests that actually execute measurements to verify settings work without bugs."""
|
|
864
|
+
|
|
865
|
+
@pytest.fixture
|
|
866
|
+
def mock_data(self):
|
|
867
|
+
"""Create mock stack, labels, and trajectories for testing."""
|
|
868
|
+
import pandas as pd
|
|
869
|
+
|
|
870
|
+
# Create a simple 5-frame, 2-channel stack (shape: T, Y, X, C)
|
|
871
|
+
np.random.seed(42)
|
|
872
|
+
stack = np.random.randint(0, 65535, (5, 100, 100, 2), dtype=np.uint16)
|
|
873
|
+
|
|
874
|
+
# Create labels with 3 cells per frame
|
|
875
|
+
labels = np.zeros((5, 100, 100), dtype=np.int32)
|
|
876
|
+
# Cell 1: centered at (25, 25)
|
|
877
|
+
labels[:, 20:30, 20:30] = 1
|
|
878
|
+
# Cell 2: centered at (50, 50)
|
|
879
|
+
labels[:, 45:55, 45:55] = 2
|
|
880
|
+
# Cell 3: centered at (75, 75)
|
|
881
|
+
labels[:, 70:80, 70:80] = 3
|
|
882
|
+
|
|
883
|
+
# Create trajectories DataFrame matching the labels
|
|
884
|
+
trajectories = pd.DataFrame(
|
|
885
|
+
{
|
|
886
|
+
"TRACK_ID": [1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3],
|
|
887
|
+
"FRAME": [0, 1, 2, 3, 4] * 3,
|
|
888
|
+
"POSITION_X": [25] * 5 + [50] * 5 + [75] * 5,
|
|
889
|
+
"POSITION_Y": [25] * 5 + [50] * 5 + [75] * 5,
|
|
890
|
+
"class_id": [1] * 15, # Required by measure()
|
|
891
|
+
}
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
channel_names = ["DAPI", "GFP"]
|
|
895
|
+
|
|
896
|
+
return stack, labels, trajectories, channel_names
|
|
897
|
+
|
|
898
|
+
def test_basic_features_measurement(self, mock_data):
|
|
899
|
+
"""Test measuring basic features (area, intensity)."""
|
|
900
|
+
from celldetective.measure import measure
|
|
901
|
+
|
|
902
|
+
stack, labels, trajectories, channel_names = mock_data
|
|
903
|
+
|
|
904
|
+
result = measure(
|
|
905
|
+
stack=stack,
|
|
906
|
+
labels=labels,
|
|
907
|
+
trajectories=trajectories,
|
|
908
|
+
channel_names=channel_names,
|
|
909
|
+
features=["area", "intensity_mean"],
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
assert result is not None
|
|
913
|
+
assert len(result) == 15 # 3 cells * 5 frames
|
|
914
|
+
assert "area" in result.columns
|
|
915
|
+
# Intensity should be prefixed with channel name
|
|
916
|
+
assert any("intensity_mean" in col or "DAPI" in col for col in result.columns)
|
|
917
|
+
|
|
918
|
+
def test_isotropic_intensity_measurement(self, mock_data):
|
|
919
|
+
"""Test measuring isotropic intensity with radii."""
|
|
920
|
+
from celldetective.measure import measure
|
|
921
|
+
|
|
922
|
+
stack, labels, trajectories, channel_names = mock_data
|
|
923
|
+
|
|
924
|
+
result = measure(
|
|
925
|
+
stack=stack,
|
|
926
|
+
labels=labels,
|
|
927
|
+
trajectories=trajectories,
|
|
928
|
+
channel_names=channel_names,
|
|
929
|
+
features=["area"],
|
|
930
|
+
intensity_measurement_radii=[5, 10],
|
|
931
|
+
isotropic_operations=["mean", "sum"],
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
assert result is not None
|
|
935
|
+
assert len(result) == 15
|
|
936
|
+
# Should have columns for circle intensities
|
|
937
|
+
cols = result.columns.tolist()
|
|
938
|
+
assert any("circle" in col.lower() or "5" in col for col in cols)
|
|
939
|
+
|
|
940
|
+
def test_border_distance_measurement(self, mock_data):
|
|
941
|
+
"""Test measuring at border distances (contour measurements)."""
|
|
942
|
+
from celldetective.measure import measure
|
|
943
|
+
|
|
944
|
+
stack, labels, trajectories, channel_names = mock_data
|
|
945
|
+
|
|
946
|
+
result = measure(
|
|
947
|
+
stack=stack,
|
|
948
|
+
labels=labels,
|
|
949
|
+
trajectories=trajectories,
|
|
950
|
+
channel_names=channel_names,
|
|
951
|
+
features=["area", "intensity_mean"],
|
|
952
|
+
border_distances=[5, 10],
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
assert result is not None
|
|
956
|
+
assert len(result) == 15
|
|
957
|
+
# Should have columns for edge measurements
|
|
958
|
+
cols = result.columns.tolist()
|
|
959
|
+
assert any(
|
|
960
|
+
"edge" in col.lower() or "border" in col.lower() or "5px" in col.lower()
|
|
961
|
+
for col in cols
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
def test_haralick_texture_measurement(self, mock_data):
|
|
965
|
+
"""Test measuring Haralick texture features."""
|
|
966
|
+
from celldetective.measure import measure
|
|
967
|
+
|
|
968
|
+
stack, labels, trajectories, channel_names = mock_data
|
|
969
|
+
|
|
970
|
+
haralick_options = {
|
|
971
|
+
"channel": "DAPI",
|
|
972
|
+
"channel_index": 0,
|
|
973
|
+
"distance": 1,
|
|
974
|
+
"n_intensity_bins": 64,
|
|
975
|
+
"percentile_mode": True,
|
|
976
|
+
"percentile_min": 0.01,
|
|
977
|
+
"percentile_max": 99.9,
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
result = measure(
|
|
981
|
+
stack=stack,
|
|
982
|
+
labels=labels,
|
|
983
|
+
trajectories=trajectories,
|
|
984
|
+
channel_names=channel_names,
|
|
985
|
+
features=["area"],
|
|
986
|
+
haralick_options=haralick_options,
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
assert result is not None
|
|
990
|
+
assert len(result) == 15
|
|
991
|
+
# Haralick features may or may not be computed depending on image content
|
|
992
|
+
# (random noise may not have enough texture). Just verify no errors.
|
|
993
|
+
|
|
994
|
+
def test_combined_settings_measurement(self, mock_data):
|
|
995
|
+
"""Test measuring with multiple settings combined."""
|
|
996
|
+
from celldetective.measure import measure
|
|
997
|
+
|
|
998
|
+
stack, labels, trajectories, channel_names = mock_data
|
|
999
|
+
|
|
1000
|
+
haralick_options = {
|
|
1001
|
+
"channel": "DAPI",
|
|
1002
|
+
"channel_index": 0,
|
|
1003
|
+
"distance": 1,
|
|
1004
|
+
"n_intensity_bins": 32,
|
|
1005
|
+
"percentile_mode": True,
|
|
1006
|
+
"percentile_min": 0.01,
|
|
1007
|
+
"percentile_max": 99.9,
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
result = measure(
|
|
1011
|
+
stack=stack,
|
|
1012
|
+
labels=labels,
|
|
1013
|
+
trajectories=trajectories,
|
|
1014
|
+
channel_names=channel_names,
|
|
1015
|
+
features=["area", "intensity_mean", "perimeter"],
|
|
1016
|
+
intensity_measurement_radii=[5],
|
|
1017
|
+
isotropic_operations=["mean"],
|
|
1018
|
+
border_distances=[5],
|
|
1019
|
+
haralick_options=haralick_options,
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
assert result is not None
|
|
1023
|
+
assert len(result) == 15
|
|
1024
|
+
assert "area" in result.columns
|
|
1025
|
+
assert "TRACK_ID" in result.columns
|
|
1026
|
+
assert "FRAME" in result.columns
|
|
1027
|
+
|
|
1028
|
+
def test_measurement_without_stack(self, mock_data):
|
|
1029
|
+
"""Test measuring features from labels only (no intensity)."""
|
|
1030
|
+
from celldetective.measure import measure
|
|
1031
|
+
|
|
1032
|
+
_, labels, trajectories, _ = mock_data
|
|
1033
|
+
|
|
1034
|
+
result = measure(
|
|
1035
|
+
stack=None,
|
|
1036
|
+
labels=labels,
|
|
1037
|
+
trajectories=trajectories,
|
|
1038
|
+
features=["area", "perimeter", "eccentricity"],
|
|
1039
|
+
)
|
|
1040
|
+
|
|
1041
|
+
assert result is not None
|
|
1042
|
+
assert len(result) == 15
|
|
1043
|
+
assert "area" in result.columns
|
|
1044
|
+
|
|
1045
|
+
def test_measurement_empty_features_list(self, mock_data):
|
|
1046
|
+
"""Test measuring with empty features list (isotropic only)."""
|
|
1047
|
+
from celldetective.measure import measure
|
|
1048
|
+
|
|
1049
|
+
stack, labels, trajectories, channel_names = mock_data
|
|
1050
|
+
|
|
1051
|
+
result = measure(
|
|
1052
|
+
stack=stack,
|
|
1053
|
+
labels=labels,
|
|
1054
|
+
trajectories=trajectories,
|
|
1055
|
+
channel_names=channel_names,
|
|
1056
|
+
features=[],
|
|
1057
|
+
intensity_measurement_radii=[5],
|
|
1058
|
+
isotropic_operations=["mean"],
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
assert result is not None
|
|
1062
|
+
assert len(result) == 15
|
|
1063
|
+
|
|
1064
|
+
def test_multiple_radii_and_operations(self, mock_data):
|
|
1065
|
+
"""Test with multiple radii and operations."""
|
|
1066
|
+
from celldetective.measure import measure
|
|
1067
|
+
|
|
1068
|
+
stack, labels, trajectories, channel_names = mock_data
|
|
1069
|
+
|
|
1070
|
+
result = measure(
|
|
1071
|
+
stack=stack,
|
|
1072
|
+
labels=labels,
|
|
1073
|
+
trajectories=trajectories,
|
|
1074
|
+
channel_names=channel_names,
|
|
1075
|
+
features=["area"],
|
|
1076
|
+
intensity_measurement_radii=[3, 5, 10, 15],
|
|
1077
|
+
isotropic_operations=["mean", "sum", "std"], # Avoid min/max edge cases
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1080
|
+
assert result is not None
|
|
1081
|
+
assert len(result) == 15
|
|
1082
|
+
# Should have many columns from the combinations
|
|
1083
|
+
assert len(result.columns) > 10
|