phasor-handler 2.2.4__tar.gz → 2.3.0__tar.gz
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.
- {phasor_handler-2.2.4/phasor_handler.egg-info → phasor_handler-2.3.0}/PKG-INFO +1 -1
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/environment.yml +0 -1
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/__init__.py +1 -1
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/app.py +25 -4
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/widgets/__init__.py +2 -1
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/widgets/analysis/components/circle_roi.py +307 -26
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/widgets/analysis/components/meta_info.py +4 -1
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/widgets/analysis/components/roi_list.py +177 -15
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/widgets/analysis/components/trace_plot.py +2 -2
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/widgets/analysis/view.py +108 -9
- phasor_handler-2.3.0/phasor_handler/widgets/secondlevel/view.py +659 -0
- phasor_handler-2.3.0/phasor_handler/workers/__init__.py +4 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/workers/conversion_worker.py +4 -1
- phasor_handler-2.3.0/phasor_handler/workers/secondlevel_worker.py +148 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0/phasor_handler.egg-info}/PKG-INFO +1 -1
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler.egg-info/SOURCES.txt +3 -1
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/pyproject.toml +1 -1
- phasor_handler-2.2.4/phasor_handler/workers/__init__.py +0 -3
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/CONTRIBUTING.md +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/LICENSE.md +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/MANIFEST.in +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/README.md +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/img/icons/chevron-down.svg +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/img/icons/chevron-up.svg +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/img/logo.ico +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/models/dir_manager.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/scripts/contrast.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/scripts/convert.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/scripts/meta_reader.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/scripts/plot.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/scripts/register.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/themes/__init__.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/themes/dark_theme.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/tools/__init__.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/tools/check_stylesheet.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/tools/misc.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/widgets/analysis/components/__init__.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/widgets/analysis/components/bnc.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/widgets/analysis/components/image_view.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/widgets/conversion/view.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/widgets/registration/view.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/workers/analysis_worker.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/workers/histogram_worker.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler/workers/registration_worker.py +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler.egg-info/dependency_links.txt +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler.egg-info/entry_points.txt +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler.egg-info/requires.txt +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/phasor_handler.egg-info/top_level.txt +0 -0
- {phasor_handler-2.2.4 → phasor_handler-2.3.0}/setup.cfg +0 -0
|
@@ -4,6 +4,6 @@ Phasor Handler - Two-Photon Phasor Imaging Data Processor
|
|
|
4
4
|
A PyQt6 GUI toolbox for processing two-photon phasor imaging data.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "2.
|
|
7
|
+
__version__ = "2.3.0"
|
|
8
8
|
__author__ = "Josia Shemuel"
|
|
9
9
|
__all__ = ["app", "widgets", "workers", "models", "scripts", "tools", "themes"]
|
|
@@ -13,7 +13,7 @@ from PyQt6.QtGui import QFileSystemModel, QIcon
|
|
|
13
13
|
from PyQt6.QtCore import Qt
|
|
14
14
|
from PyQt6.QtCore import QThread
|
|
15
15
|
|
|
16
|
-
from .widgets import ConversionWidget, RegistrationWidget, AnalysisWidget
|
|
16
|
+
from .widgets import ConversionWidget, RegistrationWidget, AnalysisWidget, SecondLevelWidget
|
|
17
17
|
from .workers import RegistrationWorker, ConversionWorker
|
|
18
18
|
from .models.dir_manager import DirManager
|
|
19
19
|
from .themes import apply_dark_theme
|
|
@@ -37,7 +37,8 @@ class MainWindow(QMainWindow):
|
|
|
37
37
|
self.tabs.addTab(ConversionWidget(self), "Conversion")
|
|
38
38
|
self.tabs.addTab(RegistrationWidget(self), "Registration")
|
|
39
39
|
# AnalysisWidget exposes compatible attributes on the main window
|
|
40
|
-
self.tabs.addTab(AnalysisWidget(self), "
|
|
40
|
+
self.tabs.addTab(AnalysisWidget(self), "First Level")
|
|
41
|
+
self.tabs.addTab(SecondLevelWidget(self), "Second Level")
|
|
41
42
|
self.tabs.currentChanged.connect(self.on_tab_changed)
|
|
42
43
|
self.setCentralWidget(self.tabs)
|
|
43
44
|
|
|
@@ -130,17 +131,37 @@ class MainWindow(QMainWindow):
|
|
|
130
131
|
self.analysis_list_widget.addItem(item)
|
|
131
132
|
|
|
132
133
|
def on_tab_changed(self, idx):
|
|
133
|
-
# When switching to
|
|
134
|
+
# When switching to first level tab, refresh its directory list
|
|
134
135
|
tab_text = self.tabs.tabText(idx)
|
|
135
|
-
if tab_text == "
|
|
136
|
+
if tab_text == "First Level":
|
|
136
137
|
if hasattr(self, 'analysis_list_widget'):
|
|
138
|
+
# Capture current selection to persist it
|
|
139
|
+
current_path = None
|
|
140
|
+
selected_items = self.analysis_list_widget.selectedItems()
|
|
141
|
+
if selected_items:
|
|
142
|
+
current_path = selected_items[0].data(Qt.ItemDataRole.UserRole)
|
|
143
|
+
|
|
144
|
+
# Block signals to prevent unnecessary clearing/reloading of the image
|
|
145
|
+
self.analysis_list_widget.blockSignals(True)
|
|
137
146
|
self.analysis_list_widget.clear()
|
|
147
|
+
|
|
138
148
|
from PyQt6.QtWidgets import QListWidgetItem
|
|
139
149
|
for full_path, display_name in self.dir_manager.get_display_names():
|
|
140
150
|
item = QListWidgetItem(display_name)
|
|
141
151
|
item.setToolTip(full_path)
|
|
142
152
|
item.setData(Qt.ItemDataRole.UserRole, full_path)
|
|
143
153
|
self.analysis_list_widget.addItem(item)
|
|
154
|
+
|
|
155
|
+
# Restore selection if it matches
|
|
156
|
+
if current_path is not None and full_path == current_path:
|
|
157
|
+
item.setSelected(True)
|
|
158
|
+
self.analysis_list_widget.setCurrentItem(item)
|
|
159
|
+
|
|
160
|
+
self.analysis_list_widget.blockSignals(False)
|
|
161
|
+
elif tab_text == "Second Level":
|
|
162
|
+
# Trigger refresh of second level plots when tab is shown
|
|
163
|
+
if hasattr(self, 'second_level_widget'):
|
|
164
|
+
self.second_level_widget.refresh_plots()
|
|
144
165
|
|
|
145
166
|
def run_conversion_script(self):
|
|
146
167
|
if not self.selected_dirs:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from .analysis.view import AnalysisWidget
|
|
2
2
|
from .conversion.view import ConversionWidget
|
|
3
3
|
from .registration.view import RegistrationWidget
|
|
4
|
+
from .secondlevel.view import SecondLevelWidget
|
|
4
5
|
|
|
5
|
-
__all__ = ["AnalysisWidget", "ConversionWidget", "RegistrationWidget"]
|
|
6
|
+
__all__ = ["AnalysisWidget", "ConversionWidget", "RegistrationWidget", "SecondLevelWidget"]
|
|
@@ -21,6 +21,7 @@ class CircleRoiTool(QObject):
|
|
|
21
21
|
roiChanged = pyqtSignal(tuple) # (x0, y0, x1, y1) in image coords (during drag)
|
|
22
22
|
roiFinalized = pyqtSignal(tuple) # (x0, y0, x1, y1) in image coords (on release)
|
|
23
23
|
roiSelected = pyqtSignal(int) # index of ROI selected by right-click
|
|
24
|
+
roiSelectionToggled = pyqtSignal(int) # index of ROI to toggle selection for (Shift+Click)
|
|
24
25
|
roiDrawingStarted = pyqtSignal() # emitted when user starts drawing a new ROI
|
|
25
26
|
|
|
26
27
|
def __init__(self, label: QLabel, parent=None):
|
|
@@ -77,6 +78,11 @@ class CircleRoiTool(QObject):
|
|
|
77
78
|
self._freehand_points_origin = None # List of original freehand points for transformations
|
|
78
79
|
# whether to show the small mode text in the overlay (can be toggled by view)
|
|
79
80
|
self._show_mode_text = True
|
|
81
|
+
|
|
82
|
+
# Multi-selection support for moving multiple ROIs simultaneously
|
|
83
|
+
self._selected_roi_indices = [] # List of indices of selected ROIs
|
|
84
|
+
self._multi_roi_origins = None # Dict storing original positions during multi-ROI drag
|
|
85
|
+
self._multi_roi_drag_offset = None # QPointF storing current drag offset
|
|
80
86
|
|
|
81
87
|
def set_draw_rect(self, rect: QRect):
|
|
82
88
|
"""Rectangle where the scaled pixmap is drawn inside the label."""
|
|
@@ -146,6 +152,95 @@ class CircleRoiTool(QObject):
|
|
|
146
152
|
if self._base_pixmap is not None:
|
|
147
153
|
self._paint_overlay()
|
|
148
154
|
|
|
155
|
+
def finalize_multi_roi_movement(self):
|
|
156
|
+
"""Finalize the multi-ROI movement by applying the offset to saved ROIs."""
|
|
157
|
+
if self._multi_roi_origins is not None and self._multi_roi_drag_offset is not None:
|
|
158
|
+
dx = self._multi_roi_drag_offset.x()
|
|
159
|
+
dy = self._multi_roi_drag_offset.y()
|
|
160
|
+
|
|
161
|
+
# Apply changes to all selected ROIs
|
|
162
|
+
for idx, origin in self._multi_roi_origins.items():
|
|
163
|
+
if 0 <= idx < len(self._saved_rois):
|
|
164
|
+
roi = self._saved_rois[idx]
|
|
165
|
+
ox, oy, ow, oh = origin['bbox']
|
|
166
|
+
new_left = ox + dx
|
|
167
|
+
new_top = oy + dy
|
|
168
|
+
|
|
169
|
+
# Convert back to image coordinates
|
|
170
|
+
if self._draw_rect and self._img_w and self._img_h:
|
|
171
|
+
scale_x = self._img_w / float(self._draw_rect.width())
|
|
172
|
+
scale_y = self._img_h / float(self._draw_rect.height())
|
|
173
|
+
|
|
174
|
+
img_x0 = (new_left - self._draw_rect.left()) * scale_x
|
|
175
|
+
img_y0 = (new_top - self._draw_rect.top()) * scale_y
|
|
176
|
+
img_x1 = img_x0 + (ow * scale_x)
|
|
177
|
+
img_y1 = img_y0 + (oh * scale_y)
|
|
178
|
+
|
|
179
|
+
# Update the ROI's xyxy coordinates
|
|
180
|
+
roi['xyxy'] = (img_x0, img_y0, img_x1, img_y1)
|
|
181
|
+
|
|
182
|
+
# If this ROI has freehand points, translate them too
|
|
183
|
+
if origin.get('freehand_points'):
|
|
184
|
+
original_points = origin['freehand_points']
|
|
185
|
+
translated_points = []
|
|
186
|
+
for pt in original_points:
|
|
187
|
+
# Points are stored in image coordinates
|
|
188
|
+
# Convert to label coordinates, translate, convert back
|
|
189
|
+
label_x = self._draw_rect.left() + (pt[0] / scale_x)
|
|
190
|
+
label_y = self._draw_rect.top() + (pt[1] / scale_y)
|
|
191
|
+
new_label_x = label_x + dx
|
|
192
|
+
new_label_y = label_y + dy
|
|
193
|
+
new_img_x = (new_label_x - self._draw_rect.left()) * scale_x
|
|
194
|
+
new_img_y = (new_label_y - self._draw_rect.top()) * scale_y
|
|
195
|
+
translated_points.append((new_img_x, new_img_y))
|
|
196
|
+
roi['points'] = translated_points
|
|
197
|
+
|
|
198
|
+
# Clear the origin state
|
|
199
|
+
self._multi_roi_origins = None
|
|
200
|
+
self._multi_roi_drag_offset = None
|
|
201
|
+
self._paint_overlay()
|
|
202
|
+
print(f"DEBUG: Multi-ROI movement finalized for {len(self._selected_roi_indices)} ROIs")
|
|
203
|
+
return True
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
def revert_multi_roi_movement(self):
|
|
207
|
+
"""Revert the multi-ROI movement back to original positions."""
|
|
208
|
+
if self._multi_roi_origins is not None and len(self._multi_roi_origins) > 0:
|
|
209
|
+
# Restore original positions for all moved ROIs
|
|
210
|
+
for idx, origin in self._multi_roi_origins.items():
|
|
211
|
+
if 0 <= idx < len(self._saved_rois):
|
|
212
|
+
roi = self._saved_rois[idx]
|
|
213
|
+
ox, oy, ow, oh = origin['bbox']
|
|
214
|
+
|
|
215
|
+
# Convert original bbox back to image coordinates
|
|
216
|
+
if self._draw_rect and self._img_w and self._img_h:
|
|
217
|
+
scale_x = self._img_w / float(self._draw_rect.width())
|
|
218
|
+
scale_y = self._img_h / float(self._draw_rect.height())
|
|
219
|
+
|
|
220
|
+
img_x0 = (ox - self._draw_rect.left()) * scale_x
|
|
221
|
+
img_y0 = (oy - self._draw_rect.top()) * scale_y
|
|
222
|
+
img_x1 = img_x0 + (ow * scale_x)
|
|
223
|
+
img_y1 = img_y0 + (oh * scale_y)
|
|
224
|
+
|
|
225
|
+
# Restore original xyxy
|
|
226
|
+
roi['xyxy'] = (img_x0, img_y0, img_x1, img_y1)
|
|
227
|
+
|
|
228
|
+
# Restore original freehand points if they existed
|
|
229
|
+
if origin.get('freehand_points'):
|
|
230
|
+
roi['points'] = origin['freehand_points']
|
|
231
|
+
|
|
232
|
+
# Clear the origin state
|
|
233
|
+
self._multi_roi_origins = None
|
|
234
|
+
self._multi_roi_drag_offset = None
|
|
235
|
+
self._paint_overlay()
|
|
236
|
+
print(f"DEBUG: Multi-ROI movement reverted for {len(self._selected_roi_indices)} ROIs")
|
|
237
|
+
return True
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
def is_multi_roi_preview_active(self):
|
|
241
|
+
"""Check if there's a pending multi-ROI movement preview."""
|
|
242
|
+
return self._multi_roi_origins is not None and len(self._multi_roi_origins) > 0
|
|
243
|
+
|
|
149
244
|
def _get_bbox_center(self):
|
|
150
245
|
"""Get the center point of the current bbox."""
|
|
151
246
|
if self._bbox is None:
|
|
@@ -272,8 +367,22 @@ class CircleRoiTool(QObject):
|
|
|
272
367
|
else:
|
|
273
368
|
print("Draw an ROI first before switching interaction modes")
|
|
274
369
|
return True
|
|
370
|
+
elif event.key() == Qt.Key.Key_R:
|
|
371
|
+
# Finalize multi-ROI movement if active
|
|
372
|
+
if self._multi_roi_drag_offset is not None and self._multi_roi_origins:
|
|
373
|
+
self.finalize_multi_roi_movement()
|
|
374
|
+
# End the drag operation
|
|
375
|
+
self._dragging = False
|
|
376
|
+
self._mode = None
|
|
377
|
+
self._translate_anchor = None
|
|
378
|
+
return True
|
|
275
379
|
|
|
276
380
|
if et == event.Type.MouseButtonPress:
|
|
381
|
+
# Check for pending multi-ROI preview and cancel it if clicking elsewhere to start new drawing
|
|
382
|
+
if self._multi_roi_origins is not None:
|
|
383
|
+
print("DEBUG: Cancelling multi-ROI preview due to new Left Click")
|
|
384
|
+
self.revert_multi_roi_movement()
|
|
385
|
+
|
|
277
386
|
if event.button() == Qt.MouseButton.LeftButton and self._in_draw_rect(event.position()):
|
|
278
387
|
# Emit signal that ROI drawing has started
|
|
279
388
|
self.roiDrawingStarted.emit()
|
|
@@ -315,6 +424,51 @@ class CircleRoiTool(QObject):
|
|
|
315
424
|
if event.button() == Qt.MouseButton.RightButton and self._mode != 'draw':
|
|
316
425
|
p = event.position() # QPointF
|
|
317
426
|
|
|
427
|
+
# Check if we clicked on an ROI
|
|
428
|
+
clicked_roi_index = self._find_roi_at_point(p)
|
|
429
|
+
|
|
430
|
+
# Check for pending multi-ROI preview
|
|
431
|
+
if self._multi_roi_origins is not None:
|
|
432
|
+
# If we click right button again, we might want to cancel the previous preview
|
|
433
|
+
# UNLESS we are clicking on the ghost? But ghosts aren't clickable objects yet.
|
|
434
|
+
# User said "reset translate from original", so cancel is appropriate.
|
|
435
|
+
print("DEBUG: Cancelling multi-ROI preview due to new Right Click")
|
|
436
|
+
self.revert_multi_roi_movement()
|
|
437
|
+
|
|
438
|
+
# Check for Shift+Click (Multi-selection toggle or add)
|
|
439
|
+
# User Requirement: "The Shift button needs to be HOLD DOWN... It's not just a toggle"
|
|
440
|
+
# This implies explicit check for modifier.
|
|
441
|
+
if clicked_roi_index is not None and (event.modifiers() & Qt.KeyboardModifier.ShiftModifier):
|
|
442
|
+
print(f"DEBUG: Shift+RightClick on ROI {clicked_roi_index} - toggling selection")
|
|
443
|
+
self.roiSelectionToggled.emit(clicked_roi_index)
|
|
444
|
+
return True
|
|
445
|
+
|
|
446
|
+
# Check if multiple ROIs are selected AND we clicked on one of them
|
|
447
|
+
if len(self._selected_roi_indices) > 1 and clicked_roi_index is not None and clicked_roi_index in self._selected_roi_indices:
|
|
448
|
+
# Multi-selection mode - move all selected ROIs together
|
|
449
|
+
print(f"DEBUG: Multi-ROI drag started - moving {len(self._selected_roi_indices)} ROIs")
|
|
450
|
+
self._mode = 'translate'
|
|
451
|
+
self._dragging = True
|
|
452
|
+
self._translate_anchor = p
|
|
453
|
+
self._multi_roi_drag_offset = QPointF(0, 0)
|
|
454
|
+
|
|
455
|
+
# Store original positions of all selected ROIs
|
|
456
|
+
self._multi_roi_origins = {}
|
|
457
|
+
for idx in self._selected_roi_indices:
|
|
458
|
+
if 0 <= idx < len(self._saved_rois):
|
|
459
|
+
roi = self._saved_rois[idx]
|
|
460
|
+
xyxy = roi.get('xyxy')
|
|
461
|
+
if xyxy:
|
|
462
|
+
lbbox = self._label_bbox_from_image_xyxy(xyxy)
|
|
463
|
+
if lbbox:
|
|
464
|
+
self._multi_roi_origins[idx] = {
|
|
465
|
+
'bbox': lbbox,
|
|
466
|
+
'rotation': roi.get('rotation', 0.0),
|
|
467
|
+
'freehand_points': roi.get('points', None)
|
|
468
|
+
}
|
|
469
|
+
return True
|
|
470
|
+
|
|
471
|
+
# Single selection mode - original behavior
|
|
318
472
|
# First, check if we're clicking on any saved ROI
|
|
319
473
|
roi_index = self._find_roi_at_point(p)
|
|
320
474
|
if roi_index is not None:
|
|
@@ -425,31 +579,47 @@ class CircleRoiTool(QObject):
|
|
|
425
579
|
self.roiChanged.emit(xyxy)
|
|
426
580
|
return True
|
|
427
581
|
|
|
428
|
-
elif self._mode == 'translate' and self.
|
|
429
|
-
#
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
new_top = oy + dy
|
|
436
|
-
|
|
437
|
-
self._bbox = (new_left, new_top, ow, oh)
|
|
438
|
-
|
|
439
|
-
# Also translate freehand points if they exist
|
|
440
|
-
if self._freehand_points_origin:
|
|
441
|
-
self._freehand_points = []
|
|
442
|
-
for pt in self._freehand_points_origin:
|
|
443
|
-
new_pt = QPointF(pt.x() + dx, pt.y() + dy)
|
|
444
|
-
self._freehand_points.append(new_pt)
|
|
582
|
+
elif self._mode == 'translate' and self._translate_anchor is not None:
|
|
583
|
+
# Check if we're in multi-ROI mode
|
|
584
|
+
if self._multi_roi_origins is not None and len(self._multi_roi_origins) > 0:
|
|
585
|
+
# Multi-ROI translation mode - PREVIEW ONLY
|
|
586
|
+
anchor = self._translate_anchor
|
|
587
|
+
dx = pos.x() - anchor.x()
|
|
588
|
+
dy = pos.y() - anchor.y()
|
|
445
589
|
|
|
446
|
-
#
|
|
447
|
-
self.
|
|
590
|
+
# Update offset
|
|
591
|
+
self._multi_roi_drag_offset = QPointF(dx, dy)
|
|
592
|
+
|
|
593
|
+
# Repaint to show preview
|
|
594
|
+
self._paint_overlay()
|
|
595
|
+
return True
|
|
448
596
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
self.
|
|
597
|
+
# Single ROI translation (original behavior)
|
|
598
|
+
if self._bbox_origin is not None:
|
|
599
|
+
# compute delta and move bbox using float math to avoid size drift
|
|
600
|
+
anchor = self._translate_anchor
|
|
601
|
+
dx = pos.x() - anchor.x()
|
|
602
|
+
dy = pos.y() - anchor.y()
|
|
603
|
+
ox, oy, ow, oh = self._bbox_origin
|
|
604
|
+
new_left = ox + dx
|
|
605
|
+
new_top = oy + dy
|
|
606
|
+
|
|
607
|
+
self._bbox = (new_left, new_top, ow, oh)
|
|
608
|
+
|
|
609
|
+
# Also translate freehand points if they exist
|
|
610
|
+
if self._freehand_points_origin:
|
|
611
|
+
self._freehand_points = []
|
|
612
|
+
for pt in self._freehand_points_origin:
|
|
613
|
+
new_pt = QPointF(pt.x() + dx, pt.y() + dy)
|
|
614
|
+
self._freehand_points.append(new_pt)
|
|
615
|
+
|
|
616
|
+
# Recalculate bbox from translated points to ensure center alignment
|
|
617
|
+
self._update_bbox_from_freehand_points()
|
|
618
|
+
|
|
619
|
+
self._paint_overlay()
|
|
620
|
+
xyxy = self._current_roi_image_coords()
|
|
621
|
+
if xyxy is not None:
|
|
622
|
+
self.roiChanged.emit(xyxy)
|
|
453
623
|
return True
|
|
454
624
|
|
|
455
625
|
elif self._mode == 'rotate' and self._rotation_anchor is not None:
|
|
@@ -539,6 +709,21 @@ class CircleRoiTool(QObject):
|
|
|
539
709
|
|
|
540
710
|
if self._mode == 'translate' and event.button() == Qt.MouseButton.RightButton:
|
|
541
711
|
self._dragging = False
|
|
712
|
+
|
|
713
|
+
# Check if we're in multi-ROI mode
|
|
714
|
+
# Check if we're in multi-ROI mode
|
|
715
|
+
if self._multi_roi_origins is not None and len(self._multi_roi_origins) > 0:
|
|
716
|
+
# Mouse released -> KEEP PREVIEW
|
|
717
|
+
self._translate_anchor = None
|
|
718
|
+
self._mode = None
|
|
719
|
+
self._dragging = False # Stop dragging but keep state
|
|
720
|
+
|
|
721
|
+
# Repaint to keep preview visible
|
|
722
|
+
self._paint_overlay()
|
|
723
|
+
print(f"DEBUG: Multi-ROI drag released - preview kept. Press 'R' to commit, click elsewhere to cancel.")
|
|
724
|
+
return True
|
|
725
|
+
|
|
726
|
+
# Single ROI translation finalization (original behavior)
|
|
542
727
|
# finalize translation
|
|
543
728
|
self._translate_anchor = None
|
|
544
729
|
self._bbox_origin = None
|
|
@@ -764,7 +949,11 @@ class CircleRoiTool(QObject):
|
|
|
764
949
|
lx0, ly0, lw, lh = lbbox
|
|
765
950
|
px0 = float(lx0) - offset_x
|
|
766
951
|
py0 = float(ly0) - offset_y
|
|
767
|
-
|
|
952
|
+
|
|
953
|
+
# Check if this ROI is selected
|
|
954
|
+
is_selected = idx in self._selected_roi_indices
|
|
955
|
+
|
|
956
|
+
# determine color - use brighter/thicker pen for selected ROIs
|
|
768
957
|
col = saved.get('color')
|
|
769
958
|
if isinstance(col, QColor):
|
|
770
959
|
qcol = col
|
|
@@ -773,8 +962,16 @@ class CircleRoiTool(QObject):
|
|
|
773
962
|
qcol = QColor(int(col[0]), int(col[1]), int(col[2]), int(a))
|
|
774
963
|
else:
|
|
775
964
|
qcol = QColor(200, 100, 10, 200)
|
|
776
|
-
|
|
777
|
-
|
|
965
|
+
|
|
966
|
+
# Highlight selected ROIs with thicker, brighter border
|
|
967
|
+
if is_selected:
|
|
968
|
+
# Use a brighter version of the ROI's own color
|
|
969
|
+
spen = QPen(qcol.lighter(150))
|
|
970
|
+
spen.setColor(QColor(spen.color().red(), spen.color().green(), spen.color().blue(), 255)) # Ensure opaque
|
|
971
|
+
spen.setWidth(5)
|
|
972
|
+
else:
|
|
973
|
+
spen = QPen(qcol)
|
|
974
|
+
spen.setWidth(3)
|
|
778
975
|
painter.setPen(spen)
|
|
779
976
|
|
|
780
977
|
# Check ROI type
|
|
@@ -874,6 +1071,75 @@ class CircleRoiTool(QObject):
|
|
|
874
1071
|
except Exception:
|
|
875
1072
|
pass
|
|
876
1073
|
|
|
1074
|
+
# Draw Multi-ROI Drag Preview (Ghosts)
|
|
1075
|
+
if self._multi_roi_origins is not None and self._multi_roi_drag_offset is not None:
|
|
1076
|
+
try:
|
|
1077
|
+
dx = self._multi_roi_drag_offset.x()
|
|
1078
|
+
dy = self._multi_roi_drag_offset.y()
|
|
1079
|
+
|
|
1080
|
+
# Use a distinct pen for ghosts
|
|
1081
|
+
ghost_pen = QPen(QColor(255, 255, 0, 200)) # Yellow
|
|
1082
|
+
ghost_pen.setWidth(2)
|
|
1083
|
+
ghost_pen.setStyle(Qt.PenStyle.DashLine)
|
|
1084
|
+
painter.setPen(ghost_pen)
|
|
1085
|
+
|
|
1086
|
+
for idx, origin in self._multi_roi_origins.items():
|
|
1087
|
+
ox, oy, ow, oh = origin['bbox']
|
|
1088
|
+
|
|
1089
|
+
# Apply offset
|
|
1090
|
+
ghost_left = ox + dx
|
|
1091
|
+
ghost_top = oy + dy
|
|
1092
|
+
|
|
1093
|
+
# Draw ghost based on type
|
|
1094
|
+
if 0 <= idx < len(self._saved_rois):
|
|
1095
|
+
roi = self._saved_rois[idx]
|
|
1096
|
+
roi_type = roi.get('type', 'circular')
|
|
1097
|
+
rotation = roi.get('rotation', 0.0)
|
|
1098
|
+
|
|
1099
|
+
if roi_type == 'freehand' and origin.get('freehand_points'):
|
|
1100
|
+
# Draw freehand ghost
|
|
1101
|
+
if self._draw_rect and self._img_w and self._img_h:
|
|
1102
|
+
polygon = QPolygonF()
|
|
1103
|
+
scale_x = self._img_w / float(self._draw_rect.width())
|
|
1104
|
+
scale_y = self._img_h / float(self._draw_rect.height())
|
|
1105
|
+
|
|
1106
|
+
for pt in origin['freehand_points']:
|
|
1107
|
+
# Image -> Label
|
|
1108
|
+
lx = self._draw_rect.left() + (pt[0] / scale_x)
|
|
1109
|
+
ly = self._draw_rect.top() + (pt[1] / scale_y)
|
|
1110
|
+
# Apply drag offset
|
|
1111
|
+
lx += dx
|
|
1112
|
+
ly += dy
|
|
1113
|
+
# Label -> Pixmap
|
|
1114
|
+
px = lx - offset_x
|
|
1115
|
+
py = ly - offset_y
|
|
1116
|
+
polygon.append(QPointF(px, py))
|
|
1117
|
+
painter.drawPolygon(polygon)
|
|
1118
|
+
else:
|
|
1119
|
+
# Draw Rect/Ellipse ghost
|
|
1120
|
+
px = ghost_left - offset_x
|
|
1121
|
+
py = ghost_top - offset_y
|
|
1122
|
+
|
|
1123
|
+
if rotation != 0.0:
|
|
1124
|
+
center_x = px + ow / 2.0
|
|
1125
|
+
center_y = py + oh / 2.0
|
|
1126
|
+
painter.save()
|
|
1127
|
+
painter.translate(center_x, center_y)
|
|
1128
|
+
painter.rotate(math.degrees(rotation))
|
|
1129
|
+
if roi_type == 'rectangular':
|
|
1130
|
+
painter.drawRect(int(round(-ow/2)), int(round(-oh/2)), int(round(ow)), int(round(oh)))
|
|
1131
|
+
else:
|
|
1132
|
+
painter.drawEllipse(int(round(-ow/2)), int(round(-oh/2)), int(round(ow)), int(round(oh)))
|
|
1133
|
+
painter.restore()
|
|
1134
|
+
else:
|
|
1135
|
+
if roi_type == 'rectangular':
|
|
1136
|
+
painter.drawRect(int(round(px)), int(round(py)), int(round(ow)), int(round(oh)))
|
|
1137
|
+
else:
|
|
1138
|
+
painter.drawEllipse(int(round(px)), int(round(py)), int(round(ow)), int(round(oh)))
|
|
1139
|
+
|
|
1140
|
+
except Exception as e:
|
|
1141
|
+
print(f"Error drawing multi-ROI preview: {e}")
|
|
1142
|
+
|
|
877
1143
|
# Draw stimulus ROIs with distinctive styling (if visible)
|
|
878
1144
|
if getattr(self, '_show_stim_rois', True):
|
|
879
1145
|
try:
|
|
@@ -937,6 +1203,21 @@ class CircleRoiTool(QObject):
|
|
|
937
1203
|
font.setBold(True)
|
|
938
1204
|
painter.setFont(font)
|
|
939
1205
|
painter.drawText(10, 25, mode_text)
|
|
1206
|
+
|
|
1207
|
+
# Draw multi-selection indicator when multiple ROIs are selected
|
|
1208
|
+
if len(self._selected_roi_indices) > 1 and getattr(self, '_show_mode_text', True):
|
|
1209
|
+
# Check if we're in preview mode (after drag, before finalize)
|
|
1210
|
+
if self._multi_roi_origins is not None and len(self._multi_roi_origins) > 0:
|
|
1211
|
+
multi_text = f"Preview: {len(self._selected_roi_indices)} ROIs moved - Press 'R' to finalize, Escape to revert"
|
|
1212
|
+
painter.setPen(QPen(QColor(255, 165, 0, 255))) # Orange for preview
|
|
1213
|
+
else:
|
|
1214
|
+
multi_text = f"Selected: {len(self._selected_roi_indices)} ROIs (right-click drag to move all)"
|
|
1215
|
+
painter.setPen(QPen(QColor(255, 255, 0, 255))) # Bright yellow
|
|
1216
|
+
font = QFont()
|
|
1217
|
+
font.setPointSize(11)
|
|
1218
|
+
font.setBold(True)
|
|
1219
|
+
painter.setFont(font)
|
|
1220
|
+
painter.drawText(10, 50, multi_text)
|
|
940
1221
|
|
|
941
1222
|
painter.end()
|
|
942
1223
|
self._label.setPixmap(overlay)
|
|
@@ -291,7 +291,7 @@ class MetadataViewer(QDialog):
|
|
|
291
291
|
self.add_info_row(self.stim_layout, row, "Camera Frame Rate (Hz)", str(camera_framerate))
|
|
292
292
|
else:
|
|
293
293
|
# 3i: Show stimulation information
|
|
294
|
-
stim_keys = ['stimulation_timeframes', 'stimulation_ms', 'duty_cycle', 'stimulated_roi_location', 'stimulated_rois']
|
|
294
|
+
stim_keys = ['stimulation_timeframes', 'stimulation_ms', 'duty_cycle', 'stimulated_roi_location', 'stimulated_rois', 'stimulated_roi_powers']
|
|
295
295
|
for key in stim_keys:
|
|
296
296
|
if key in self.metadata:
|
|
297
297
|
value = self.metadata[key]
|
|
@@ -315,6 +315,9 @@ class MetadataViewer(QDialog):
|
|
|
315
315
|
else:
|
|
316
316
|
display_value = ', '.join([', '.join(map(str, x)) for x in value])
|
|
317
317
|
key = "Stimulated ROIs"
|
|
318
|
+
elif key == 'stimulated_roi_powers':
|
|
319
|
+
display_value = ', '.join(map(str, value))
|
|
320
|
+
key = "Stimulated ROI Powers"
|
|
318
321
|
else:
|
|
319
322
|
display_value = str(value)
|
|
320
323
|
self.add_info_row(self.stim_layout, row, key.replace('_', ' '),
|