OSCR

<i>Drosophila</i> RSK: A Pivotal Regulator of Circadian Plasticity at the Neuronal and Behavioral Level.

Code ↔ Paper

1 match between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 1 match
  1. [1] § Materials and Methods › Quantification of s-LNv Arborization ↔ src/MorphoScope.py, lines 532–575 · score 0.63 · graphical user interface, MorphoScope, dimensions, quantify

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 2,304 lines · 91 KB · MIT · 1 match

  1. """
  2. MorphoScope - Main Window Implementation
  3. This module contains the main window class for the MorphoScope application,
  4. a tool for quantifying structural plasticity in 3D microscopy images of
  5. neuronal projections.
  6. Features:
  7. - Multi-format image loading (CZI, TIF, LSM)
  8. - Interactive ROI selection
  9. - Image filtering and preprocessing
  10. - Structural plasticity quantification
  11. - Batch processing and CSV export
  12. Author: Francisco Tassara
  13. Date: 2025-11-12
  14. Based on: Petsakou, Sapsis & Blau, Cell 2015
  15. """
  16. from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
  17. QMetaObject, QObject, QPoint, QRect,
  18. QSize, QTime, QUrl, Qt)
  19. from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
  20. QFont, QFontDatabase, QGradient, QIcon,
  21. QImage, QKeySequence, QLinearGradient, QPainter,
  22. QPalette, QPixmap, QRadialGradient, QTransform)
  23. from PySide6.QtWidgets import (QApplication, QComboBox, QGroupBox, QHBoxLayout,
  24. QLabel, QLayout, QLineEdit, QListWidget, QCheckBox,
  25. QListWidgetItem, QMainWindow, QPushButton, QRadioButton,
  26. QSizePolicy, QSpacerItem, QSpinBox, QTextEdit,
  27. QVBoxLayout, QWidget, QProgressDialog)
  28. from pyqtgraph import ImageView
  29. import pyqtgraph
  30. from PySide6.QtWidgets import QMessageBox, QFileDialog, QInputDialog
  31. import sys
  32. import os
  33. import numpy as np
  34. from matplotlib.path import Path as MPLPath
  35. from scipy import ndimage as ndi
  36. from shapely.geometry import Polygon
  37. import csv
  38. import matplotlib.pyplot as plt
  39. import tifffile
  40. from pylibCZIrw import czi
  41. from scipy.ndimage import median_filter
  42. from skimage.filters import gaussian
  43. from config import Config, setup_logging
  44. from image_processor import ImageProcessor, validate_parameters
  45. from typing import Optional, Tuple, List
  46. # Configurar logging
  47. import logging
  48. setup_logging()
  49. logger = logging.getLogger(__name__)
  50. class Ui_MainWindow(object):
  51. def setupUi(self, MainWindow):
  52. if not MainWindow.objectName():
  53. MainWindow.setObjectName(u"MainWindow")
  54. MainWindow.resize(1316, 815)
  55. icon = QIcon()
  56. icon.addFile(u"logo.ico", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
  57. MainWindow.setWindowIcon(icon)
  58. self.centralwidget = QWidget(MainWindow)
  59. self.centralwidget.setObjectName(u"centralwidget")
  60. self.horizontalLayout_10 = QHBoxLayout(self.centralwidget)
  61. self.horizontalLayout_10.setObjectName(u"horizontalLayout_10")
  62. self.verticalLayout_11 = QVBoxLayout()
  63. self.verticalLayout_11.setObjectName(u"verticalLayout_11")
  64. self.horizontalLayout_7 = QHBoxLayout()
  65. self.horizontalLayout_7.setObjectName(u"horizontalLayout_7")
  66. self.pushButton_loadImage = QPushButton(self.centralwidget)
  67. self.pushButton_loadImage.setObjectName(u"pushButton_loadImage")
  68. font = QFont()
  69. font.setPointSize(10)
  70. font.setBold(True)
  71. self.pushButton_loadImage.setFont(font)
  72. self.horizontalLayout_7.addWidget(self.pushButton_loadImage)
  73. self.pushButton_clean = QPushButton(self.centralwidget)
  74. self.pushButton_clean.setObjectName(u"pushButton_clean")
  75. font1 = QFont()
  76. font1.setPointSize(10)
  77. self.pushButton_clean.setFont(font1)
  78. self.horizontalLayout_7.addWidget(self.pushButton_clean)
  79. self.verticalLayout_11.addLayout(self.horizontalLayout_7)
  80. self.listWidget_images = QListWidget(self.centralwidget)
  81. self.listWidget_images.setObjectName(u"listWidget_images")
  82. self.verticalLayout_11.addWidget(self.listWidget_images)
  83. self.groupBox_properties = QGroupBox(self.centralwidget)
  84. self.groupBox_properties.setObjectName(u"groupBox_properties")
  85. self.groupBox_properties.setFont(font1)
  86. self.horizontalLayout = QHBoxLayout(self.groupBox_properties)
  87. self.horizontalLayout.setObjectName(u"horizontalLayout")
  88. self.verticalLayout_10 = QVBoxLayout()
  89. self.verticalLayout_10.setObjectName(u"verticalLayout_10")
  90. self.label_5 = QLabel(self.groupBox_properties)
  91. self.label_5.setObjectName(u"label_5")
  92. self.verticalLayout_10.addWidget(self.label_5)
  93. self.label_2 = QLabel(self.groupBox_properties)
  94. self.label_2.setObjectName(u"label_2")
  95. self.verticalLayout_10.addWidget(self.label_2)
  96. self.horizontalLayout.addLayout(self.verticalLayout_10)
  97. self.verticalLayout_3 = QVBoxLayout()
  98. self.verticalLayout_3.setObjectName(u"verticalLayout_3")
  99. self.lineEdit_image_size_X = QLineEdit(self.groupBox_properties)
  100. self.lineEdit_image_size_X.setObjectName(u"lineEdit_image_size_X")
  101. self.verticalLayout_3.addWidget(self.lineEdit_image_size_X)
  102. self.lineEdit_pixel_size_X = QLineEdit(self.groupBox_properties)
  103. self.lineEdit_pixel_size_X.setObjectName(u"lineEdit_pixel_size_X")
  104. self.verticalLayout_3.addWidget(self.lineEdit_pixel_size_X)
  105. self.horizontalLayout.addLayout(self.verticalLayout_3)
  106. self.verticalLayout_5 = QVBoxLayout()
  107. self.verticalLayout_5.setObjectName(u"verticalLayout_5")
  108. self.label_6 = QLabel(self.groupBox_properties)
  109. self.label_6.setObjectName(u"label_6")
  110. self.verticalLayout_5.addWidget(self.label_6)
  111. self.label_9 = QLabel(self.groupBox_properties)
  112. self.label_9.setObjectName(u"label_9")
  113. self.verticalLayout_5.addWidget(self.label_9)
  114. self.horizontalLayout.addLayout(self.verticalLayout_5)
  115. self.verticalLayout_6 = QVBoxLayout()
  116. self.verticalLayout_6.setObjectName(u"verticalLayout_6")
  117. self.lineEdit_image_size_Y = QLineEdit(self.groupBox_properties)
  118. self.lineEdit_image_size_Y.setObjectName(u"lineEdit_image_size_Y")
  119. self.verticalLayout_6.addWidget(self.lineEdit_image_size_Y)
  120. self.lineEdit_pixel_size_Y = QLineEdit(self.groupBox_properties)
  121. self.lineEdit_pixel_size_Y.setObjectName(u"lineEdit_pixel_size_Y")
  122. self.verticalLayout_6.addWidget(self.lineEdit_pixel_size_Y)
  123. self.horizontalLayout.addLayout(self.verticalLayout_6)
  124. self.verticalLayout_7 = QVBoxLayout()
  125. self.verticalLayout_7.setObjectName(u"verticalLayout_7")
  126. self.label_7 = QLabel(self.groupBox_properties)
  127. self.label_7.setObjectName(u"label_7")
  128. self.verticalLayout_7.addWidget(self.label_7)
  129. self.label_10 = QLabel(self.groupBox_properties)
  130. self.label_10.setObjectName(u"label_10")
  131. self.verticalLayout_7.addWidget(self.label_10)
  132. self.horizontalLayout.addLayout(self.verticalLayout_7)
  133. self.verticalLayout_8 = QVBoxLayout()
  134. self.verticalLayout_8.setObjectName(u"verticalLayout_8")
  135. self.lineEdit_image_size_Z = QLineEdit(self.groupBox_properties)
  136. self.lineEdit_image_size_Z.setObjectName(u"lineEdit_image_size_Z")
  137. self.verticalLayout_8.addWidget(self.lineEdit_image_size_Z)
  138. self.lineEdit_pixel_size_Z = QLineEdit(self.groupBox_properties)
  139. self.lineEdit_pixel_size_Z.setObjectName(u"lineEdit_pixel_size_Z")
  140. self.verticalLayout_8.addWidget(self.lineEdit_pixel_size_Z)
  141. self.horizontalLayout.addLayout(self.verticalLayout_8)
  142. self.verticalLayout_9 = QVBoxLayout()
  143. self.verticalLayout_9.setObjectName(u"verticalLayout_9")
  144. self.label_8 = QLabel(self.groupBox_properties)
  145. self.label_8.setObjectName(u"label_8")
  146. self.verticalLayout_9.addWidget(self.label_8)
  147. self.label_11 = QLabel(self.groupBox_properties)
  148. self.label_11.setObjectName(u"label_11")
  149. self.verticalLayout_9.addWidget(self.label_11)
  150. self.horizontalLayout.addLayout(self.verticalLayout_9)
  151. self.verticalLayout_11.addWidget(self.groupBox_properties)
  152. self.groupBox_visualizer = QGroupBox(self.centralwidget)
  153. self.groupBox_visualizer.setObjectName(u"groupBox_visualizer")
  154. self.groupBox_visualizer.setFont(font1)
  155. self.verticalLayout = QVBoxLayout(self.groupBox_visualizer)
  156. self.verticalLayout.setObjectName(u"verticalLayout")
  157. self.horizontalLayout_4 = QHBoxLayout()
  158. self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
  159. self.label_4 = QLabel(self.groupBox_visualizer)
  160. self.label_4.setObjectName(u"label_4")
  161. self.horizontalLayout_4.addWidget(self.label_4)
  162. self.comboBox_channel_selector = QComboBox(self.groupBox_visualizer)
  163. self.comboBox_channel_selector.setObjectName(u"comboBox_channel_selector")
  164. self.horizontalLayout_4.addWidget(self.comboBox_channel_selector)
  165. self.verticalLayout.addLayout(self.horizontalLayout_4)
  166. self.horizontalLayout_6 = QHBoxLayout()
  167. self.horizontalLayout_6.setObjectName(u"horizontalLayout_6")
  168. self.label_12 = QLabel(self.groupBox_visualizer)
  169. self.label_12.setObjectName(u"label_12")
  170. self.horizontalLayout_6.addWidget(self.label_12)
  171. self.radioButton_plotZproject = QRadioButton(self.groupBox_visualizer)
  172. self.radioButton_plotZproject.setObjectName(u"radioButton_plotZproject")
  173. self.radioButton_plotZproject.setChecked(True)
  174. self.horizontalLayout_6.addWidget(self.radioButton_plotZproject)
  175. self.radioButton_plotStack = QRadioButton(self.groupBox_visualizer)
  176. self.radioButton_plotStack.setObjectName(u"radioButton_plotStack")
  177. self.horizontalLayout_6.addWidget(self.radioButton_plotStack)
  178. self.verticalLayout.addLayout(self.horizontalLayout_6)
  179. self.verticalLayout_11.addWidget(self.groupBox_visualizer)
  180. self.groupBox_channels = QGroupBox(self.centralwidget)
  181. self.groupBox_channels.setObjectName(u"groupBox_channels")
  182. font2 = QFont()
  183. font2.setPointSize(10)
  184. font2.setBold(False)
  185. self.groupBox_channels.setFont(font2)
  186. self.verticalLayout_2 = QVBoxLayout(self.groupBox_channels)
  187. self.verticalLayout_2.setObjectName(u"verticalLayout_2")
  188. self.horizontalLayout_2 = QHBoxLayout()
  189. self.horizontalLayout_2.setSpacing(1)
  190. self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
  191. self.label = QLabel(self.groupBox_channels)
  192. self.label.setObjectName(u"label")
  193. self.horizontalLayout_2.addWidget(self.label)
  194. self.comboBox_plasticityChannel = QComboBox(self.groupBox_channels)
  195. self.comboBox_plasticityChannel.setObjectName(u"comboBox_plasticityChannel")
  196. self.horizontalLayout_2.addWidget(self.comboBox_plasticityChannel)
  197. self.comboBox_filter_type_chP = QComboBox(self.groupBox_channels)
  198. self.comboBox_filter_type_chP.addItem("")
  199. self.comboBox_filter_type_chP.addItem("")
  200. self.comboBox_filter_type_chP.addItem("")
  201. self.comboBox_filter_type_chP.addItem("")
  202. self.comboBox_filter_type_chP.addItem("")
  203. self.comboBox_filter_type_chP.setObjectName(u"comboBox_filter_type_chP")
  204. self.horizontalLayout_2.addWidget(self.comboBox_filter_type_chP)
  205. self.pushButton_applyfilter_chP = QPushButton(self.groupBox_channels)
  206. self.pushButton_applyfilter_chP.setObjectName(u"pushButton_applyfilter_chP")
  207. self.horizontalLayout_2.addWidget(self.pushButton_applyfilter_chP)
  208. self.pushButton_undofilter_chP = QPushButton(self.groupBox_channels)
  209. self.pushButton_undofilter_chP.setObjectName(u"pushButton_undofilter_chP")
  210. self.horizontalLayout_2.addWidget(self.pushButton_undofilter_chP)
  211. self.horizontalLayout_2.setStretch(0, 1)
  212. self.horizontalLayout_2.setStretch(1, 5)
  213. self.horizontalLayout_2.setStretch(2, 1)
  214. self.horizontalLayout_2.setStretch(3, 1)
  215. self.horizontalLayout_2.setStretch(4, 1)
  216. self.verticalLayout_2.addLayout(self.horizontalLayout_2)
  217. self.horizontalLayout_3 = QHBoxLayout()
  218. self.horizontalLayout_3.setSpacing(1)
  219. self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
  220. self.horizontalLayout_3.setSizeConstraint(QLayout.SizeConstraint.SetDefaultConstraint)
  221. self.label_3 = QLabel(self.groupBox_channels)
  222. self.label_3.setObjectName(u"label_3")
  223. self.horizontalLayout_3.addWidget(self.label_3)
  224. self.comboBox_fluoChannel = QComboBox(self.groupBox_channels)
  225. self.comboBox_fluoChannel.setObjectName(u"comboBox_fluoChannel")
  226. self.horizontalLayout_3.addWidget(self.comboBox_fluoChannel)
  227. self.comboBox_filter_type_chF = QComboBox(self.groupBox_channels)
  228. self.comboBox_filter_type_chF.addItem("")
  229. self.comboBox_filter_type_chF.addItem("")
  230. self.comboBox_filter_type_chF.addItem("")
  231. self.comboBox_filter_type_chF.addItem("")
  232. self.comboBox_filter_type_chF.addItem("")
  233. self.comboBox_filter_type_chF.setObjectName(u"comboBox_filter_type_chF")
  234. self.horizontalLayout_3.addWidget(self.comboBox_filter_type_chF)
  235. self.pushButton_applyfilter_chF = QPushButton(self.groupBox_channels)
  236. self.pushButton_applyfilter_chF.setObjectName(u"pushButton_applyfilter_chF")
  237. self.horizontalLayout_3.addWidget(self.pushButton_applyfilter_chF)
  238. self.pushButton_undofilter_chF = QPushButton(self.groupBox_channels)
  239. self.pushButton_undofilter_chF.setObjectName(u"pushButton_undofilter_chF")
  240. self.horizontalLayout_3.addWidget(self.pushButton_undofilter_chF)
  241. self.horizontalLayout_3.setStretch(0, 1)
  242. self.horizontalLayout_3.setStretch(1, 5)
  243. self.horizontalLayout_3.setStretch(2, 1)
  244. self.horizontalLayout_3.setStretch(3, 1)
  245. self.horizontalLayout_3.setStretch(4, 1)
  246. self.verticalLayout_2.addLayout(self.horizontalLayout_3)
  247. self.verticalLayout_11.addWidget(self.groupBox_channels)
  248. self.groupBox_observations = QGroupBox(self.centralwidget)
  249. self.groupBox_observations.setObjectName(u"groupBox_observations")
  250. self.groupBox_observations.setFont(font1)
  251. self.verticalLayout_4 = QVBoxLayout(self.groupBox_observations)
  252. self.verticalLayout_4.setObjectName(u"verticalLayout_4")
  253. self.textEdit_observation = QTextEdit(self.groupBox_observations)
  254. self.textEdit_observation.setObjectName(u"textEdit_observation")
  255. self.textEdit_observation.setFont(font1)
  256. self.verticalLayout_4.addWidget(self.textEdit_observation)
  257. self.verticalLayout_11.addWidget(self.groupBox_observations)
  258. self.groupBox_roi = QGroupBox(self.centralwidget)
  259. self.groupBox_roi.setObjectName(u"groupBox_roi")
  260. self.groupBox_roi.setFont(font1)
  261. self.horizontalLayout_5 = QHBoxLayout(self.groupBox_roi)
  262. self.horizontalLayout_5.setObjectName(u"horizontalLayout_5")
  263. self.pushButton_roi = QPushButton(self.groupBox_roi)
  264. self.pushButton_roi.setObjectName(u"pushButton_roi")
  265. self.pushButton_roi.setFont(font1)
  266. self.horizontalLayout_5.addWidget(self.pushButton_roi)
  267. self.pushButton_apply_roi = QPushButton(self.groupBox_roi)
  268. self.pushButton_apply_roi.setObjectName(u"pushButton_apply_roi")
  269. self.pushButton_apply_roi.setFont(font1)
  270. self.horizontalLayout_5.addWidget(self.pushButton_apply_roi)
  271. self.verticalLayout_11.addWidget(self.groupBox_roi)
  272. self.horizontalLayout_8 = QHBoxLayout()
  273. self.horizontalLayout_8.setObjectName(u"horizontalLayout_8")
  274. self.groupBox = QGroupBox(self.centralwidget)
  275. self.groupBox.setObjectName(u"groupBox")
  276. self.groupBox.setFont(font1)
  277. self.horizontalLayout_9 = QHBoxLayout(self.groupBox)
  278. self.horizontalLayout_9.setObjectName(u"horizontalLayout_9")
  279. self.horizontalLayout_9.setContentsMargins(-1, -1, -1, 3)
  280. self.label_13 = QLabel(self.groupBox)
  281. self.label_13.setObjectName(u"label_13")
  282. self.horizontalLayout_9.addWidget(self.label_13)
  283. self.spinBox_zmin = QSpinBox(self.groupBox)
  284. self.spinBox_zmin.setObjectName(u"spinBox_zmin")
  285. self.horizontalLayout_9.addWidget(self.spinBox_zmin)
  286. self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
  287. self.horizontalLayout_9.addItem(self.horizontalSpacer_2)
  288. self.label_14 = QLabel(self.groupBox)
  289. self.label_14.setObjectName(u"label_14")
  290. self.horizontalLayout_9.addWidget(self.label_14)
  291. self.spinBox_zmax = QSpinBox(self.groupBox)
  292. self.spinBox_zmax.setObjectName(u"spinBox_zmax")
  293. self.horizontalLayout_9.addWidget(self.spinBox_zmax)
  294. self.horizontalLayout_9.setStretch(0, 1)
  295. self.horizontalLayout_9.setStretch(1, 1)
  296. self.horizontalLayout_9.setStretch(2, 4)
  297. self.horizontalLayout_9.setStretch(3, 1)
  298. self.horizontalLayout_9.setStretch(4, 1)
  299. self.horizontalLayout_8.addWidget(self.groupBox)
  300. self.checkBox_show_distributions = QCheckBox(self.centralwidget)
  301. self.checkBox_show_distributions.setObjectName(u"checkBox_show_distributions")
  302. self.horizontalLayout_8.addWidget(self.checkBox_show_distributions)
  303. self.verticalLayout_11.addLayout(self.horizontalLayout_8)
  304. self.pushButton_procces = QPushButton(self.centralwidget)
  305. self.pushButton_procces.setObjectName(u"pushButton_procces")
  306. font3 = QFont()
  307. font3.setPointSize(12)
  308. font3.setBold(True)
  309. self.pushButton_procces.setFont(font3)
  310. self.verticalLayout_11.addWidget(self.pushButton_procces)
  311. self.verticalLayout_11.setStretch(0, 1)
  312. self.verticalLayout_11.setStretch(1, 2)
  313. self.verticalLayout_11.setStretch(2, 1)
  314. self.verticalLayout_11.setStretch(3, 1)
  315. self.verticalLayout_11.setStretch(4, 1)
  316. self.verticalLayout_11.setStretch(5, 1)
  317. self.verticalLayout_11.setStretch(6, 1)
  318. self.verticalLayout_11.setStretch(7, 1)
  319. self.verticalLayout_11.setStretch(8, 1)
  320. self.horizontalLayout_10.addLayout(self.verticalLayout_11)
  321. self.graphWidget = ImageView(self.centralwidget)
  322. self.graphWidget.setObjectName(u"graphWidget")
  323. self.graphWidget.setStyleSheet(u"background-color: rgb(0, 0, 0);")
  324. self.horizontalLayout_10.addWidget(self.graphWidget)
  325. self.horizontalLayout_10.setStretch(0, 1)
  326. self.horizontalLayout_10.setStretch(1, 2)
  327. MainWindow.setCentralWidget(self.centralwidget)
  328. self.retranslateUi(MainWindow)
  329. QMetaObject.connectSlotsByName(MainWindow)
  330. # setupUi
  331. def retranslateUi(self, MainWindow):
  332. MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MorphoScope", None))
  333. self.pushButton_loadImage.setText(QCoreApplication.translate("MainWindow", u"Load images", None))
  334. self.pushButton_clean.setText(QCoreApplication.translate("MainWindow", u"Clean", None))
  335. self.groupBox_properties.setTitle(QCoreApplication.translate("MainWindow", u"Image Properties", None))
  336. self.label_5.setText(QCoreApplication.translate("MainWindow", u"Image Size (X,Y,Z):", None))
  337. self.label_2.setText(QCoreApplication.translate("MainWindow", u"Voxel Size (X,Y,Z):", None))
  338. self.lineEdit_image_size_X.setText(QCoreApplication.translate("MainWindow", u"X", None))
  339. self.lineEdit_pixel_size_X.setText(QCoreApplication.translate("MainWindow", u"X", None))
  340. self.label_6.setText(QCoreApplication.translate("MainWindow", u"x", None))
  341. self.label_9.setText(QCoreApplication.translate("MainWindow", u"x", None))
  342. self.lineEdit_image_size_Y.setText(QCoreApplication.translate("MainWindow", u"Y", None))
  343. self.lineEdit_pixel_size_Y.setText(QCoreApplication.translate("MainWindow", u"Y", None))
  344. self.label_7.setText(QCoreApplication.translate("MainWindow", u"x", None))
  345. self.label_10.setText(QCoreApplication.translate("MainWindow", u"x", None))
  346. self.lineEdit_image_size_Z.setText(QCoreApplication.translate("MainWindow", u"Z", None))
  347. self.lineEdit_pixel_size_Z.setText(QCoreApplication.translate("MainWindow", u"Z", None))
  348. self.label_8.setText(QCoreApplication.translate("MainWindow", u"px", None))
  349. self.label_11.setText(QCoreApplication.translate("MainWindow", u"um", None))
  350. self.groupBox_visualizer.setTitle(QCoreApplication.translate("MainWindow", u"Visualizer", None))
  351. self.label_4.setText(QCoreApplication.translate("MainWindow", u"Channel:", None))
  352. self.label_12.setText(QCoreApplication.translate("MainWindow", u"Visualization Mode:", None))
  353. self.radioButton_plotZproject.setText(QCoreApplication.translate("MainWindow", u"Z project", None))
  354. self.radioButton_plotStack.setText(QCoreApplication.translate("MainWindow", u"Stack", None))
  355. self.groupBox_channels.setTitle(QCoreApplication.translate("MainWindow", u"Channels for quantification", None))
  356. self.label.setText(QCoreApplication.translate("MainWindow", u"Plasticity: ", None))
  357. self.comboBox_filter_type_chP.setItemText(0, QCoreApplication.translate("MainWindow", u"Select Filter", None))
  358. self.comboBox_filter_type_chP.setItemText(1, QCoreApplication.translate("MainWindow", u"Threshold", None))
  359. self.comboBox_filter_type_chP.setItemText(2, QCoreApplication.translate("MainWindow", u"Gaussian Blur", None))
  360. self.comboBox_filter_type_chP.setItemText(3, QCoreApplication.translate("MainWindow", u"Median Filter", None))
  361. self.comboBox_filter_type_chP.setItemText(4, QCoreApplication.translate("MainWindow", u"Rolling Ball", None))
  362. self.pushButton_applyfilter_chP.setText(QCoreApplication.translate("MainWindow", u"Apply", None))
  363. self.pushButton_undofilter_chP.setText(QCoreApplication.translate("MainWindow", u"Undo", None))
  364. self.label_3.setText(QCoreApplication.translate("MainWindow", u"Fluoresence:", None))
  365. self.comboBox_filter_type_chF.setItemText(0, QCoreApplication.translate("MainWindow", u"Select Filter", None))
  366. self.comboBox_filter_type_chF.setItemText(1, QCoreApplication.translate("MainWindow", u"Threshold", None))
  367. self.comboBox_filter_type_chF.setItemText(2, QCoreApplication.translate("MainWindow", u"Gaussian Blur", None))
  368. self.comboBox_filter_type_chF.setItemText(3, QCoreApplication.translate("MainWindow", u"Median Filter", None))
  369. self.comboBox_filter_type_chF.setItemText(4, QCoreApplication.translate("MainWindow", u"Rolling Ball", None))
  370. self.pushButton_applyfilter_chF.setText(QCoreApplication.translate("MainWindow", u"Apply", None))
  371. self.pushButton_undofilter_chF.setText(QCoreApplication.translate("MainWindow", u"Undo", None))
  372. self.groupBox_observations.setTitle(QCoreApplication.translate("MainWindow", u"Observation", None))
  373. self.groupBox_roi.setTitle(QCoreApplication.translate("MainWindow", u"Region of Interest (ROI) Selection", None))
  374. self.pushButton_roi.setText(QCoreApplication.translate("MainWindow", u"Create ROI", None))
  375. self.pushButton_apply_roi.setText(QCoreApplication.translate("MainWindow", u"Apply ROI", None))
  376. self.groupBox.setTitle(QCoreApplication.translate("MainWindow", u"Slice Selection", None))
  377. self.label_13.setText(QCoreApplication.translate("MainWindow", u"From:", None))
  378. self.label_14.setText(QCoreApplication.translate("MainWindow", u"To:", None))
  379. #if QT_CONFIG(tooltip)
  380. self.checkBox_show_distributions.setToolTip(QCoreApplication.translate("MainWindow", u"Display X, Y, Z distribution plots after processing", None))
  381. #endif // QT_CONFIG(tooltip)
  382. self.checkBox_show_distributions.setText(QCoreApplication.translate("MainWindow", u"Show distribution plots", None))
  383. self.pushButton_procces.setText(QCoreApplication.translate("MainWindow", u"Process image and save", None))
  384. # retranslateUi
  385. class MyMainWindow(QMainWindow):
  386. """
  387. Main application window for MorphoScope structural plasticity analysis.
  388. This class provides the graphical user interface and core functionality for:
  389. - Loading and visualizing 3D microscopy images
  390. - Applying preprocessing filters
  391. - Defining regions of interest (ROI)
  392. - Calculating structural plasticity metrics
  393. - Exporting results to CSV
  394. Attributes
  395. ----------
  396. ui : Ui_MainWindow
  397. Auto-generated UI from Qt Designer (PySide6)
  398. roi : pyqtgraph.PolyLineROI
  399. Current polygonal region of interest
  400. creating_roi : bool
  401. Flag indicating if ROI creation is active
  402. current_image_data : List[np.ndarray]
  403. Loaded image data (one array per channel)
  404. current_metadata_channel : List[str]
  405. Channel names/labels
  406. current_metadata_voxel_size : Tuple[float, float, float]
  407. Voxel dimensions (x, y, z) in micrometers
  408. current_metadata_dimension : Tuple[int, int, int]
  409. Image dimensions (width, height, depth)
  410. complexity_channel : np.ndarray
  411. Masked image for plasticity analysis
  412. fluor_channel : Optional[np.ndarray]
  413. Masked image for fluorescence analysis (if available)
  414. csv_file_path : str
  415. Path to output CSV file
  416. Notes
  417. -----
  418. - The UI must be generated from Qt Designer (.ui file) using pyside6-uic
  419. - Supports CZI, TIF, and LSM microscopy formats
  420. - Uses ImageProcessor for quantification (see image_processor.py)
  421. See Also
  422. --------
  423. image_processor.ImageProcessor : Core processing engine
  424. """
  425. def __init__(self):
  426. """
  427. Initialize the main window and connect UI elements to handlers.
  428. This method:
  429. 1. Sets up the auto-generated UI
  430. 2. Connects buttons to their respective functions
  431. 3. Initializes instance variables
  432. """
  433. super().__init__()
  434. self.ui = Ui_MainWindow()
  435. self.ui.setupUi(self)
  436. # ================================================================
  437. # CONNECT UI ELEMENTS TO HANDLERS
  438. # ================================================================
  439. # Image loading and management
  440. self.ui.pushButton_loadImage.clicked.connect(self.load_images)
  441. self.ui.pushButton_clean.clicked.connect(self.clean_list)
  442. self.ui.listWidget_images.itemSelectionChanged.connect(self.on_image_selected)
  443. # ROI selection
  444. self.ui.pushButton_roi.clicked.connect(self.enable_polygonal_roi_creation)
  445. self.ui.pushButton_apply_roi.clicked.connect(self.apply_roi_mask)
  446. # Image display controls
  447. self.ui.comboBox_channel_selector.currentIndexChanged.connect(self.update_display)
  448. self.ui.radioButton_plotZproject.toggled.connect(self.update_display)
  449. self.ui.radioButton_plotStack.toggled.connect(self.update_display)
  450. # Filter controls
  451. self.ui.pushButton_applyfilter_chP.clicked.connect(
  452. lambda: self.apply_selected_filter('chP')
  453. )
  454. self.ui.pushButton_applyfilter_chF.clicked.connect(
  455. lambda: self.apply_selected_filter('chF')
  456. )
  457. self.ui.pushButton_undofilter_chP.clicked.connect(
  458. lambda: self.undo_filter('chP')
  459. )
  460. self.ui.pushButton_undofilter_chF.clicked.connect(
  461. lambda: self.undo_filter('chF')
  462. )
  463. # Processing
  464. self.ui.pushButton_procces.clicked.connect(self.process)
  465. # ================================================================
  466. # INITIALIZE INSTANCE VARIABLES
  467. # ================================================================
  468. # ROI management
  469. self.roi = None
  470. self.creating_roi = False
  471. self.temp_points = []
  472. self.polygon_points = []
  473. self.AArea = 0.0
  474. # Image data
  475. self.current_image_filepath = None
  476. self.current_image_data = None
  477. self.original_image_data = None # Backup for undo filter
  478. self.current_metadata_channel = None
  479. self.current_metadata_dimension = None
  480. self.current_metadata_voxel_size = None
  481. # Processed channels
  482. self.complexity_channel = None
  483. self.fluor_channel = None
  484. # Output
  485. self.csv_file_path = None
  486. logger.info("MorphoScope main window initialized")
  487. # ====================================================================
  488. # IMAGE LOADING METHODS
  489. # ====================================================================
  490. def load_images(self):
  491. """
  492. Load microscopy images and set up CSV output file.
  493. This method:
  494. 1. Opens file dialog to select image files
  495. 2. Prompts for output CSV filename
  496. 3. Creates CSV with headers if new
  497. 4. Adds images to processing list
  498. Supported Formats
  499. -----------------
  500. - .czi : Zeiss CZI files
  501. - .tif : TIFF stacks
  502. - .lsm : Zeiss LSM files
  503. Notes
  504. -----
  505. - Duplicate files are automatically filtered out
  506. - CSV headers are written only for new files
  507. - All images share the same output CSV file
  508. """
  509. logger.info("Loading images...")
  510. # Open file selection dialog
  511. file_paths, _ = QFileDialog.getOpenFileNames(
  512. None,
  513. "Select Images",
  514. "",
  515. "Image Files (*.tif *.czi *.lsm)"
  516. )
  517. if not file_paths:
  518. QMessageBox.information(
  519. None,
  520. "No Files Selected",
  521. "Please select one or more image files."
  522. )
  523. return
  524. # Get output filename from user
  525. output_file_name, ok = QInputDialog.getText(
  526. None,
  527. "Output File Name",
  528. "Enter the name of the output CSV file:"
  529. )
  530. if not ok or not output_file_name.strip():
  531. QMessageBox.warning(
  532. None,
  533. "No Output File",
  534. "You must provide a name for the output file."
  535. )
  536. return
  537. # Ensure .csv extension
  538. if not output_file_name.endswith(".csv"):
  539. output_file_name += ".csv"
  540. # Determine output folder (same as first image)
  541. output_folder = os.path.dirname(file_paths[0]) if file_paths else os.getcwd()
  542. self.csv_file_path = os.path.join(output_folder, output_file_name)
  543. file_exists = os.path.isfile(self.csv_file_path)
  544. # Create CSV and write headers if new file
  545. try:
  546. with open(self.csv_file_path, mode='a', newline='', encoding='utf-8') as csv_file:
  547. writer = csv.writer(csv_file)
  548. if not file_exists:
  549. writer.writerow([
  550. "Image filename",
  551. "Spread x [pixel]",
  552. "Spread y [pixel]",
  553. "Spread z [pixel]",
  554. "Spread x*y [pixel2]",
  555. "Spread x*y*z [pixel3]",
  556. "Spread x [um]",
  557. "Spread y [um]",
  558. "Spread z [um]",
  559. "Spread x*y [um2]",
  560. "Spread x*y*z [um3]",
  561. "Axonal Volume",
  562. "Fluorescence_px",
  563. "Fluorescence_um",
  564. "Observation"
  565. ])
  566. logger.info(f"Created new CSV file: {self.csv_file_path}")
  567. except Exception as e:
  568. logger.error(f"Failed to create CSV file: {e}")
  569. QMessageBox.critical(
  570. self,
  571. "Error Creating File",
  572. f"An error occurred while creating the file:\n{e}"
  573. )
  574. return
  575. # Filter out duplicate files
  576. existing_files = [
  577. self.ui.listWidget_images.item(i).text()
  578. for i in range(self.ui.listWidget_images.count())
  579. ]
  580. new_files = [fp for fp in file_paths if fp not in existing_files]
  581. if not new_files:
  582. QMessageBox.information(
  583. self,
  584. "No New Files",
  585. "All selected files are already loaded."
  586. )
  587. return
  588. # Add new files to list
  589. self.ui.listWidget_images.addItems(new_files)
  590. logger.info(f"Loaded {len(new_files)} new images")
  591. QMessageBox.information(
  592. None,
  593. "Images Loaded",
  594. f"Loaded {len(new_files)} images. Ready to process."
  595. )
  596. def _load_czi(self, file_path: str):
  597. """
  598. Load a Zeiss CZI microscopy file.
  599. Parameters
  600. ----------
  601. file_path : str
  602. Path to .czi file
  603. Notes
  604. -----
  605. - Extracts metadata (voxel size, dimensions, channels)
  606. - Applies zero-padding for non-square images
  607. - Transposes data to (Z, X, Y) format
  608. Raises
  609. ------
  610. ValueError
  611. If required metadata keys are missing
  612. RuntimeError
  613. If file cannot be loaded
  614. """
  615. try:
  616. logger.info(f"Loading CZI file: {file_path}")
  617. with czi.open_czi(file_path) as czifile:
  618. # Extract metadata
  619. metadata = czifile.metadata
  620. width_px = int(metadata['ImageDocument']['Metadata']['Information']['Image']['SizeX'])
  621. height_px = int(metadata['ImageDocument']['Metadata']['Information']['Image']['SizeY'])
  622. zlim = int(metadata['ImageDocument']['Metadata']['Information']['Image']['SizeZ'])
  623. voxel_size = metadata['ImageDocument']['Metadata']['Scaling']['Items']['Distance']
  624. voxel_size_x_um = float(next(
  625. item for item in voxel_size if item['@Id'] == 'X'
  626. )['Value']) * 1e6
  627. voxel_size_y_um = float(next(
  628. item for item in voxel_size if item['@Id'] == 'Y'
  629. )['Value']) * 1e6
  630. voxel_size_z_um = float(next(
  631. item for item in voxel_size if item['@Id'] == 'Z'
  632. )['Value']) * 1e6
  633. num_channels = int(
  634. metadata['ImageDocument']['Metadata']['Information']['Image'].get('SizeC', 1)
  635. )
  636. channels = [f"Channel {i+1}" for i in range(num_channels)] if num_channels > 1 else ["Channel 1"]
  637. logger.debug(f"CZI metadata: {width_px}x{height_px}x{zlim}, "
  638. f"{num_channels} channels, voxel: {voxel_size_x_um:.3f}x"
  639. f"{voxel_size_y_um:.3f}x{voxel_size_z_um:.3f} µm")
  640. # Read data for each channel
  641. data = []
  642. for i in range(num_channels):
  643. channel_data = [
  644. czifile.read(plane={"C": i, "Z": z})[:, :, 0]
  645. for z in range(zlim)
  646. ]
  647. # Apply zero-padding for non-square images
  648. if width_px != height_px:
  649. max_dim = max(width_px, height_px)
  650. channel_data = np.array(channel_data)
  651. padded_image = np.zeros(
  652. (zlim, max_dim, max_dim),
  653. dtype=channel_data.dtype
  654. )
  655. y_start = (max_dim - height_px) // 2
  656. x_start = (max_dim - width_px) // 2
  657. padded_image[:, y_start:y_start+height_px, x_start:x_start+width_px] = channel_data
  658. channel_data = padded_image
  659. logger.debug(f"Applied zero-padding: {width_px}x{height_px} -> {max_dim}x{max_dim}")
  660. # Stack and transpose to (Z, X, Y)
  661. data.append(np.stack(channel_data, axis=0).transpose(0, 2, 1))
  662. # Update dimensions if padded
  663. if width_px != height_px:
  664. width_px = max_dim
  665. height_px = max_dim
  666. # Store loaded data
  667. self.current_image_data = data
  668. self.current_metadata_channel = channels
  669. self.current_metadata_voxel_size = (voxel_size_x_um, voxel_size_y_um, voxel_size_z_um)
  670. self.current_metadata_dimension = (width_px, height_px, zlim)
  671. logger.info(f"✓ CZI file loaded successfully")
  672. except KeyError as ke:
  673. logger.error(f"Metadata key not found: {ke}")
  674. raise ValueError(f"Metadata key not found: {ke}")
  675. except Exception as e:
  676. logger.error(f"Unexpected error loading CZI: {e}", exc_info=True)
  677. raise RuntimeError(f"Unexpected error while loading CZI file: {e}")
  678. def _load_tif(self, file_path: str):
  679. """
  680. Load a TIFF microscopy file.
  681. Parameters
  682. ----------
  683. file_path : str
  684. Path to .tif file
  685. Notes
  686. -----
  687. - Attempts to read ImageJ metadata for voxel sizes
  688. - Falls back to default values if metadata unavailable
  689. - Handles 2D, 3D, and 4D TIFF formats
  690. - Transposes data to (Z, X, Y) format
  691. Warnings
  692. --------
  693. If metadata is missing, default values are used:
  694. - voxel_size_x = 0.1 µm
  695. - voxel_size_y = 0.1 µm
  696. - voxel_size_z = 1.0 µm
  697. """
  698. try:
  699. logger.info(f"Loading TIF file: {file_path}")
  700. with tifffile.TiffFile(file_path) as tiff:
  701. # Try to read ImageJ metadata
  702. try:
  703. imagej_metadata = tiff.imagej_metadata
  704. if imagej_metadata is None:
  705. raise ValueError("Metadata is not available")
  706. num_channels = imagej_metadata.get('channels', 1)
  707. voxel_size_z = float(imagej_metadata.get('spacing', 1.0))
  708. logger.debug(f"ImageJ metadata: {num_channels} channels, z-spacing: {voxel_size_z} µm")
  709. except (AttributeError, ValueError):
  710. # Use default values if metadata unavailable
  711. logger.warning("No ImageJ metadata found. Using default values.")
  712. QMessageBox.warning(
  713. None,
  714. "Metadata Warning",
  715. "No metadata found. Using default values."
  716. )
  717. num_channels = 1
  718. voxel_size_z = 1.0
  719. # Assume isotropic XY voxels
  720. voxel_size_x = 0.1
  721. voxel_size_y = voxel_size_x
  722. # Read image data
  723. full_image = tiff.asarray()
  724. dims = full_image.ndim
  725. # Handle different dimensionalities
  726. if dims == 2:
  727. # Single slice, single channel
  728. image_data = full_image[np.newaxis, :, :]
  729. elif dims == 3:
  730. # Either (Z, Y, X) or (C, Y, X)
  731. if num_channels > 1 and full_image.shape[0] == num_channels:
  732. # Multi-channel, single Z
  733. image_data = [full_image[c, :, :][np.newaxis, :, :] for c in range(num_channels)]
  734. else:
  735. # Single channel, multiple Z
  736. image_data = full_image
  737. elif dims == 4:
  738. # Multi-channel, multiple Z: (Z, C, Y, X)
  739. image_data = [full_image[:, c, :, :] for c in range(num_channels)]
  740. else:
  741. raise ValueError(f"Unexpected image dimensions: {dims}")
  742. # Transpose to (Z, X, Y)
  743. if isinstance(image_data, list):
  744. image_data = [img.transpose(0, 2, 1) for img in image_data]
  745. else:
  746. image_data = image_data.transpose(0, 2, 1)
  747. # Get dimensions
  748. if num_channels == 1:
  749. height, width = image_data.shape[1:3]
  750. zlim = image_data.shape[0]
  751. data = [image_data]
  752. else:
  753. height, width = image_data[0].shape[1:3]
  754. zlim = image_data[0].shape[0]
  755. data = image_data
  756. # Store loaded data
  757. self.current_image_data = data
  758. self.current_metadata_channel = [f"Channel {i+1}" for i in range(num_channels)]
  759. self.current_metadata_voxel_size = (voxel_size_x, voxel_size_y, voxel_size_z)
  760. self.current_metadata_dimension = (width, height, zlim)
  761. logger.info(f"✓ TIF file loaded successfully: {width}x{height}x{zlim}")
  762. QMessageBox.information(
  763. None,
  764. "TIF Loaded",
  765. f"Successfully loaded {file_path}."
  766. )
  767. except Exception as e:
  768. logger.error(f"Failed to load TIF: {e}", exc_info=True)
  769. QMessageBox.critical(
  770. None,
  771. "Error Loading TIF",
  772. f"Failed to load TIF file:\n{e}"
  773. )
  774. def _load_lsm(self, file_path: str):
  775. """
  776. Load a Zeiss LSM microscopy file.
  777. Parameters
  778. ----------
  779. file_path : str
  780. Path to .lsm file
  781. Notes
  782. -----
  783. - LSM files are a special type of TIFF with Zeiss metadata
  784. - Extracts voxel sizes from LSM metadata
  785. - Transposes data to (Z, X, Y) format
  786. """
  787. try:
  788. logger.info(f"Loading LSM file: {file_path}")
  789. with tifffile.TiffFile(file_path) as tif:
  790. # Extract LSM metadata
  791. metadata = tif.lsm_metadata
  792. num_channels = metadata['DimensionChannels']
  793. zlim = metadata['DimensionZ']
  794. width = metadata['DimensionX']
  795. height = metadata['DimensionY']
  796. # Convert voxel sizes to micrometers
  797. voxel_size_x = metadata['VoxelSizeX'] * 1e6
  798. voxel_size_y = metadata['VoxelSizeY'] * 1e6
  799. voxel_size_z = metadata['VoxelSizeZ'] * 1e6
  800. logger.debug(f"LSM metadata: {width}x{height}x{zlim}, {num_channels} channels, "
  801. f"voxel: {voxel_size_x:.3f}x{voxel_size_y:.3f}x{voxel_size_z:.3f} µm")
  802. # Read image data
  803. if num_channels == 1:
  804. # Single channel
  805. image_data = tif.asarray().transpose((0, 2, 1)) # (Z, X, Y)
  806. else:
  807. # Multi-channel: (Z, C, Y, X)
  808. full_image = tif.asarray(series=0)
  809. image_data = [
  810. full_image[:, c, :, :].transpose((0, 2, 1))
  811. for c in range(num_channels)
  812. ]
  813. # Store loaded data
  814. self.current_image_data = image_data if num_channels > 1 else [image_data]
  815. self.current_metadata_channel = [f"Channel {i+1}" for i in range(num_channels)]
  816. self.current_metadata_voxel_size = (voxel_size_x, voxel_size_y, voxel_size_z)
  817. self.current_metadata_dimension = (width, height, zlim)
  818. logger.info(f"✓ LSM file loaded successfully")
  819. QMessageBox.information(
  820. None,
  821. "LSM Loaded",
  822. f"Successfully loaded {file_path}."
  823. )
  824. except Exception as e:
  825. logger.error(f"Failed to load LSM: {e}", exc_info=True)
  826. QMessageBox.critical(
  827. None,
  828. "Error Loading LSM",
  829. f"Failed to load LSM file:\n{e}"
  830. )
  831. # ====================================================================
  832. # UI MANAGEMENT METHODS
  833. # ====================================================================
  834. def clean_list(self):
  835. """
  836. Clear all loaded images and reset the application state.
  837. This method:
  838. - Clears the image list widget
  839. - Resets all image data variables
  840. - Removes any active ROI
  841. - Clears the display
  842. - Re-enables channel selectors
  843. """
  844. logger.info("Cleaning image list and resetting state")
  845. # Clear image list
  846. self.ui.listWidget_images.clear()
  847. # Reset data
  848. self.current_image_data = None
  849. self.original_image_data = None
  850. self.current_metadata_channel = None
  851. self.current_metadata_dimension = None
  852. self.current_metadata_voxel_size = None
  853. # Re-enable channel selectors
  854. self.ui.comboBox_plasticityChannel.setEnabled(True)
  855. self.ui.comboBox_fluoChannel.setEnabled(True)
  856. # Clear display
  857. self.ui.graphWidget.clear()
  858. # Cancel ROI creation if active
  859. if self.creating_roi:
  860. self.creating_roi = False
  861. self.temp_points = []
  862. if hasattr(self, "vertex_scatter"):
  863. self.ui.graphWidget.removeItem(self.vertex_scatter)
  864. self.vertex_scatter = None
  865. self.ui.pushButton_roi.setEnabled(True)
  866. self.ui.pushButton_roi.setStyleSheet("")
  867. # Remove existing ROI
  868. self.clear_polygonal_roi()
  869. if self.roi is not None:
  870. self.ui.graphWidget.removeItem(self.roi)
  871. self.roi = None
  872. logger.info("✓ Application state reset")
  873. def reset_image_data(self):
  874. """
  875. Reset current image data variables.
  876. Used when switching between images in the list.
  877. """
  878. self.current_image_filepath = None
  879. self.current_image_data = None
  880. self.original_image_data = None
  881. self.current_metadata_channel = None
  882. self.current_metadata_dimension = None
  883. self.current_metadata_voxel_size = None
  884. self.ui.graphWidget.clear()
  885. def on_image_selected(self):
  886. """
  887. Handle image selection from the list widget.
  888. This method:
  889. 1. Resets previous image data
  890. 2. Loads the selected image file
  891. 3. Updates channel selectors
  892. 4. Displays image metadata
  893. 5. Shows initial visualization
  894. Notes
  895. -----
  896. - Maintains channel selection when switching between images of same format
  897. - Automatically detects file format and calls appropriate loader
  898. - Updates UI with image dimensions and voxel sizes
  899. """
  900. logger.info("Image selected from list")
  901. self.reset_image_data()
  902. # Get selected file
  903. selected_items = self.ui.listWidget_images.selectedItems()
  904. if not selected_items:
  905. return
  906. selected_file = selected_items[0].text()
  907. self.current_image_filepath = selected_file
  908. logger.info(f"Loading: {os.path.basename(selected_file)}")
  909. # Clear display and ROI
  910. self.ui.graphWidget.clear()
  911. # Cancel ROI creation if active
  912. if self.creating_roi:
  913. self.creating_roi = False
  914. self.temp_points = []
  915. if hasattr(self, "vertex_scatter"):
  916. self.ui.graphWidget.removeItem(self.vertex_scatter)
  917. self.vertex_scatter = None
  918. self.ui.pushButton_roi.setEnabled(True)
  919. self.ui.pushButton_roi.setStyleSheet("")
  920. self.clear_polygonal_roi()
  921. if self.roi is not None:
  922. self.ui.graphWidget.removeItem(self.roi)
  923. self.roi = None
  924. # Load image based on file extension
  925. try:
  926. if selected_file.endswith(".czi"):
  927. self._load_czi(selected_file)
  928. elif selected_file.endswith(".tif"):
  929. self._load_tif(selected_file)
  930. elif selected_file.endswith(".lsm"):
  931. self._load_lsm(selected_file)
  932. else:
  933. QMessageBox.warning(
  934. None,
  935. "Unsupported Format",
  936. f"Unsupported file format: {selected_file}"
  937. )
  938. return
  939. except Exception as e:
  940. logger.error(f"Failed to load image: {e}", exc_info=True)
  941. QMessageBox.critical(
  942. None,
  943. "Error Loading Image",
  944. f"Error loading image:\n{e}"
  945. )
  946. return
  947. # Save previous channel selections
  948. prev_index_plasticity = self.ui.comboBox_plasticityChannel.currentIndex()
  949. prev_index_fluo = self.ui.comboBox_fluoChannel.currentIndex()
  950. prev_channel_count = self.ui.comboBox_plasticityChannel.count()
  951. # Update channel selectors
  952. self.ui.comboBox_plasticityChannel.setEnabled(True)
  953. self.ui.comboBox_fluoChannel.setEnabled(True)
  954. self.ui.comboBox_channel_selector.clear()
  955. self.ui.comboBox_channel_selector.addItems(self.current_metadata_channel)
  956. self.ui.comboBox_plasticityChannel.clear()
  957. self.ui.comboBox_plasticityChannel.addItems(self.current_metadata_channel)
  958. self.ui.comboBox_fluoChannel.clear()
  959. self.ui.comboBox_fluoChannel.addItem("No channel")
  960. self.ui.comboBox_fluoChannel.addItems(self.current_metadata_channel)
  961. # Restore previous selection if possible
  962. new_channel_count = len(self.current_metadata_channel)
  963. if prev_channel_count == new_channel_count:
  964. if 0 <= prev_index_plasticity < self.ui.comboBox_plasticityChannel.count():
  965. self.ui.comboBox_plasticityChannel.setCurrentIndex(prev_index_plasticity)
  966. if 0 <= prev_index_fluo + 1 < self.ui.comboBox_fluoChannel.count():
  967. self.ui.comboBox_fluoChannel.setCurrentIndex(prev_index_fluo)
  968. else:
  969. # Default selection for different channel count
  970. self.ui.comboBox_plasticityChannel.setCurrentIndex(0)
  971. self.ui.comboBox_fluoChannel.setCurrentIndex(0)
  972. # Update metadata display
  973. image_size_x, image_size_y, image_size_z = self.current_metadata_dimension
  974. voxel_size_x, voxel_size_y, voxel_size_z = self.current_metadata_voxel_size
  975. self.ui.lineEdit_image_size_X.setText(str(image_size_x))
  976. self.ui.lineEdit_image_size_Y.setText(str(image_size_y))
  977. self.ui.lineEdit_image_size_Z.setText(str(image_size_z))
  978. self.ui.lineEdit_pixel_size_X.setText(f"{voxel_size_x:.3f}")
  979. self.ui.lineEdit_pixel_size_Y.setText(f"{voxel_size_y:.3f}")
  980. self.ui.lineEdit_pixel_size_Z.setText(f"{voxel_size_z:.3f}")
  981. self.ui.spinBox_zmax.setValue(image_size_z - 1)
  982. # Display image
  983. if self.ui.radioButton_plotZproject.isChecked():
  984. # Show maximum Z projection
  985. max_projection = np.max(self.current_image_data[0], axis=0)
  986. self.ui.graphWidget.setImage(max_projection)
  987. elif self.ui.radioButton_plotStack.isChecked():
  988. # Show full stack
  989. self.ui.graphWidget.setImage(self.current_image_data[0])
  990. else:
  991. QMessageBox.warning(
  992. self,
  993. "Display Mode",
  994. "Please select either Z-projection or Stack view."
  995. )
  996. # Hide ROI button (not needed for this application)
  997. self.ui.graphWidget.ui.roiBtn.hide()
  998. logger.info(f"✓ Image loaded and displayed: {image_size_x}x{image_size_y}x{image_size_z}")
  999. def update_display(self):
  1000. """
  1001. Update the image display based on current channel and view mode selection.
  1002. Called automatically when:
  1003. - Channel selector changes
  1004. - View mode radio button toggles (Z-projection vs Stack)
  1005. View Modes
  1006. ----------
  1007. - Z-projection: Maximum intensity projection along Z axis
  1008. - Stack: Full 3D stack (can scroll through slices)
  1009. """
  1010. # Check if image is loaded
  1011. selected_items = self.ui.listWidget_images.selectedItems()
  1012. if not selected_items:
  1013. return
  1014. selected_channel = self.ui.comboBox_channel_selector.currentIndex()
  1015. if 0 <= selected_channel < len(self.current_image_data):
  1016. channel_data = self.current_image_data[selected_channel]
  1017. if self.ui.radioButton_plotZproject.isChecked():
  1018. # Show maximum Z projection
  1019. max_projection = np.max(channel_data, axis=0)
  1020. self.ui.graphWidget.setImage(max_projection)
  1021. logger.debug(f"Displaying Z-projection of channel {selected_channel}")
  1022. elif self.ui.radioButton_plotStack.isChecked():
  1023. # Show full stack
  1024. self.ui.graphWidget.setImage(channel_data)
  1025. logger.debug(f"Displaying full stack of channel {selected_channel}")
  1026. else:
  1027. QMessageBox.warning(
  1028. self,
  1029. "Display Mode",
  1030. "Please select either Z-projection or Stack view."
  1031. )
  1032. # ====================================================================
  1033. # IMAGE FILTERING METHODS
  1034. # ====================================================================
  1035. def apply_selected_filter(self, channel: str):
  1036. """
  1037. Apply the selected filter to the specified channel.
  1038. Parameters
  1039. ----------
  1040. channel : str
  1041. Channel identifier: 'chP' (plasticity) or 'chF' (fluorescence)
  1042. Available Filters
  1043. -----------------
  1044. - Threshold: Remove pixels below percentage of maximum intensity
  1045. - Gaussian Blur: Smooth image with Gaussian kernel
  1046. - Median Filter: Remove noise with median filter
  1047. Notes
  1048. -----
  1049. - Original image is backed up before filtering (for undo)
  1050. - User is prompted for filter parameters via dialog
  1051. - Display is automatically updated after filtering
  1052. """
  1053. logger.info(f"Applying filter to channel: {channel}")
  1054. # Get channel index and filter type
  1055. if channel == 'chP':
  1056. selected_filter = self.ui.comboBox_filter_type_chP.currentText()
  1057. channel_index = self.ui.comboBox_plasticityChannel.currentIndex()
  1058. elif channel == 'chF':
  1059. selected_filter = self.ui.comboBox_filter_type_chF.currentText()
  1060. channel_index = self.ui.comboBox_fluoChannel.currentIndex() - 1
  1061. else:
  1062. QMessageBox.warning(self, "Invalid Channel", "Invalid channel specified.")
  1063. return
  1064. # Validate channel index
  1065. if not (0 <= channel_index < len(self.current_image_data)):
  1066. QMessageBox.warning(
  1067. self,
  1068. "Invalid Channel",
  1069. f"Invalid channel index: {channel_index}"
  1070. )
  1071. return
  1072. # Backup original image (for undo)
  1073. if self.original_image_data is None:
  1074. self.original_image_data = {}
  1075. if channel_index not in self.original_image_data:
  1076. self.original_image_data[channel_index] = self.current_image_data[channel_index].copy()
  1077. logger.debug(f"Backed up original image for channel {channel_index}")
  1078. # Get image copy
  1079. image = self.current_image_data[channel_index].copy()
  1080. # Apply selected filter
  1081. if selected_filter == "Threshold":
  1082. threshold, ok = QInputDialog.getDouble(
  1083. self,
  1084. "Threshold",
  1085. "Percentage of maximum (0-100):",
  1086. 3.0, 0, 100, 1
  1087. )
  1088. if ok:
  1089. max_val = np.max(image)
  1090. image[image <= (threshold / 100.0) * max_val] = 0
  1091. logger.info(f"Applied threshold: {threshold}% of max ({max_val:.2f})")
  1092. elif selected_filter == "Gaussian Blur":
  1093. sigma, ok = QInputDialog.getDouble(
  1094. self,
  1095. "Gaussian Blur",
  1096. "Sigma:",
  1097. 1.0, 0.1, 50.0, 1
  1098. )
  1099. if ok:
  1100. image = gaussian(image, sigma=sigma, preserve_range=True)
  1101. logger.info(f"Applied Gaussian blur: sigma={sigma}")
  1102. elif selected_filter == "Median Filter":
  1103. size, ok = QInputDialog.getInt(
  1104. self,
  1105. "Median Filter",
  1106. "Kernel size (odd):",
  1107. 3, 1, 99, 2
  1108. )
  1109. if ok:
  1110. filtered_slices = [
  1111. median_filter(image[z], size=size)
  1112. for z in range(image.shape[0])
  1113. ]
  1114. image = np.stack(filtered_slices, axis=0)
  1115. logger.info(f"Applied median filter: size={size}")
  1116. else:
  1117. QMessageBox.warning(self, "Invalid Filter", "Please select a valid filter.")
  1118. return
  1119. # Save filtered image and update display
  1120. self.current_image_data[channel_index] = image
  1121. self.update_display()
  1122. logger.info(f"✓ Filter applied successfully to channel {channel_index}")
  1123. def undo_filter(self, channel: str):
  1124. """
  1125. Undo the last filter applied to the specified channel.
  1126. Parameters
  1127. ----------
  1128. channel : str
  1129. Channel identifier: 'chP' (plasticity) or 'chF' (fluorescence)
  1130. Notes
  1131. -----
  1132. - Restores the original image from backup
  1133. - Removes backup after restoring
  1134. - Only one undo level is supported
  1135. """
  1136. logger.info(f"Undoing filter for channel: {channel}")
  1137. # Get channel index
  1138. if channel == 'chP':
  1139. channel_index = self.ui.comboBox_plasticityChannel.currentIndex()
  1140. elif channel == 'chF':
  1141. channel_index = self.ui.comboBox_fluoChannel.currentIndex() - 1
  1142. else:
  1143. QMessageBox.warning(self, "Invalid Channel", "Invalid channel specified.")
  1144. return
  1145. # Check if backup exists
  1146. if self.original_image_data is None or channel_index not in self.original_image_data:
  1147. QMessageBox.information(
  1148. self,
  1149. "No Backup",
  1150. "No original version saved for this channel."
  1151. )
  1152. return
  1153. # Restore original image
  1154. self.current_image_data[channel_index] = self.original_image_data[channel_index].copy()
  1155. del self.original_image_data[channel_index]
  1156. # Clean up backup dict if empty
  1157. if not self.original_image_data:
  1158. self.original_image_data = None
  1159. self.update_display()
  1160. logger.info(f"✓ Filter undone for channel {channel_index}")
  1161. # ====================================================================
  1162. # ROI SELECTION METHODS
  1163. # ====================================================================
  1164. def enable_polygonal_roi_creation(self):
  1165. """
  1166. Enable interactive polygonal ROI creation mode.
  1167. In this mode:
  1168. - Click to add vertices to polygon
  1169. - Backspace/Delete/Space to remove last vertex
  1170. - Escape to cancel ROI creation
  1171. - Apply ROI button to finalize selection
  1172. Notes
  1173. -----
  1174. - Vertices are displayed as red dots
  1175. - Button turns green while in ROI creation mode
  1176. - Mouse and keyboard events are captured
  1177. """
  1178. logger.info("Enabling polygonal ROI creation")
  1179. self.creating_roi = True
  1180. self.temp_points = []
  1181. # Create scatter plot for vertices
  1182. self.vertex_scatter = pyqtgraph.ScatterPlotItem(
  1183. size=5,
  1184. pen=pyqtgraph.mkPen(None),
  1185. brush=pyqtgraph.mkBrush(255, 0, 0, 150)
  1186. )
  1187. self.ui.graphWidget.addItem(self.vertex_scatter)
  1188. # Connect mouse click event
  1189. self.ui.graphWidget.view.scene().sigMouseClicked.connect(self.add_vertex)
  1190. # Override keyboard event handler
  1191. self.ui.graphWidget.keyPressEvent = self.handle_key_press
  1192. # Update button appearance
  1193. self.ui.pushButton_roi.setStyleSheet("background-color: lightgreen;")
  1194. self.ui.pushButton_roi.setEnabled(False)
  1195. logger.info("✓ ROI creation mode active")
  1196. def add_vertex(self, event):
  1197. """
  1198. Add a vertex to the polygon ROI on mouse click.
  1199. Parameters
  1200. ----------
  1201. event : QGraphicsSceneMouseEvent
  1202. Mouse click event
  1203. Notes
  1204. -----
  1205. - Only active when creating_roi flag is True
  1206. - Converts scene coordinates to image coordinates
  1207. - Updates vertex visualization immediately
  1208. """
  1209. # Check if ROI creation is enabled
  1210. if not getattr(self, 'creating_roi', False):
  1211. return
  1212. # Get click position in image coordinates
  1213. pos = event.scenePos()
  1214. img_pos = self.ui.graphWidget.view.mapSceneToView(pos)
  1215. self.temp_points.append((img_pos.x(), img_pos.y()))
  1216. # Update vertex visualization
  1217. self.vertex_scatter.setData(
  1218. [p[0] for p in self.temp_points],
  1219. [p[1] for p in self.temp_points]
  1220. )
  1221. logger.debug(f"Added vertex: ({img_pos.x():.1f}, {img_pos.y():.1f})")
  1222. def handle_key_press(self, event):
  1223. """
  1224. Handle keyboard events during ROI creation.
  1225. Parameters
  1226. ----------
  1227. event : QKeyEvent
  1228. Keyboard event
  1229. Key Bindings
  1230. ------------
  1231. - Backspace/Delete/Space/Left: Remove last vertex
  1232. - Escape: Cancel ROI creation
  1233. """
  1234. key = event.key()
  1235. # Remove last vertex
  1236. if key in [Qt.Key_Backspace, Qt.Key_Delete,
  1237. Qt.Key_Space, Qt.Key_Left]:
  1238. if self.temp_points:
  1239. removed_point = self.temp_points.pop()
  1240. logger.debug(f"Removed vertex: {removed_point}")
  1241. # Update visualization
  1242. self.vertex_scatter.setData(
  1243. [p[0] for p in self.temp_points],
  1244. [p[1] for p in self.temp_points]
  1245. )
  1246. else:
  1247. logger.debug("No vertices to remove")
  1248. # Cancel ROI creation
  1249. elif key == Qt.Key_Escape:
  1250. logger.info("ROI creation canceled")
  1251. self.creating_roi = False
  1252. self.temp_points = []
  1253. if hasattr(self, "vertex_scatter"):
  1254. self.ui.graphWidget.removeItem(self.vertex_scatter)
  1255. self.vertex_scatter = None
  1256. # Restore button
  1257. self.ui.pushButton_roi.setEnabled(True)
  1258. self.ui.pushButton_roi.setStyleSheet("")
  1259. def clear_polygonal_roi(self):
  1260. """
  1261. Clear all temporary ROI data.
  1262. Removes:
  1263. - Vertex scatter plot
  1264. - Temporary point list
  1265. """
  1266. if hasattr(self, 'vertex_scatter') and self.vertex_scatter is not None:
  1267. self.ui.graphWidget.removeItem(self.vertex_scatter)
  1268. self.vertex_scatter = None
  1269. self.temp_points = []
  1270. def apply_roi_mask(self):
  1271. """
  1272. Apply the defined ROI mask to the selected channels.
  1273. This method:
  1274. 1. Finalizes the polygon ROI
  1275. 2. Calculates ROI area
  1276. 3. Creates binary mask
  1277. 4. Applies mask to plasticity channel
  1278. 5. Applies mask to fluorescence channel (if selected)
  1279. 6. Updates display
  1280. 7. Disables channel selectors (locked for processing)
  1281. Notes
  1282. -----
  1283. - Minimum 3 vertices required for valid polygon
  1284. - Mask is 2D, replicated across all Z slices
  1285. - Masked regions are set to zero
  1286. - Original images remain unchanged (copies are masked)
  1287. Raises
  1288. ------
  1289. Warning
  1290. If no ROI defined or less than 3 vertices
  1291. """
  1292. logger.info("Applying ROI mask...")
  1293. # Validate ROI
  1294. if self.roi is None:
  1295. if not hasattr(self, 'temp_points') or len(self.temp_points) < 3:
  1296. QMessageBox.warning(
  1297. self,
  1298. "No ROI",
  1299. "Please select an ROI or create one with at least 3 vertices."
  1300. )
  1301. return
  1302. # Close polygon automatically
  1303. self.temp_points.append(self.temp_points[0])
  1304. self.roi = pyqtgraph.PolyLineROI(
  1305. self.temp_points,
  1306. closed=True,
  1307. pen=pyqtgraph.mkPen('g', width=2)
  1308. )
  1309. self.ui.graphWidget.addItem(self.roi)
  1310. # Deactivate ROI creation mode
  1311. self.creating_roi = False
  1312. self.ui.pushButton_roi.setEnabled(True)
  1313. self.ui.pushButton_roi.setStyleSheet("")
  1314. # Get ROI vertices
  1315. roi_positions = self.roi.getLocalHandlePositions()
  1316. vertices = [(pos.x(), pos.y()) for name, pos in roi_positions]
  1317. # Calculate area and store vertices
  1318. polygon = np.array(vertices + [vertices[0]]) # Close polygon
  1319. self.AArea = Polygon(polygon).area
  1320. self.polygon_points = vertices
  1321. logger.info(f"ROI area: {self.AArea:.2f} pixels²")
  1322. # Get selected channels
  1323. plasticity_channel_index = self.ui.comboBox_plasticityChannel.currentIndex()
  1324. self.ui.comboBox_plasticityChannel.setEnabled(False) # Lock selection
  1325. fluo_channel_index = self.ui.comboBox_fluoChannel.currentIndex()
  1326. self.ui.comboBox_fluoChannel.setEnabled(False) # Lock selection
  1327. # Validate plasticity channel
  1328. if 0 <= plasticity_channel_index < len(self.current_image_data):
  1329. plasticity_image = self.current_image_data[plasticity_channel_index]
  1330. else:
  1331. QMessageBox.warning(
  1332. self,
  1333. "Invalid Channel",
  1334. "Invalid plasticity channel selected."
  1335. )
  1336. return
  1337. # Create binary mask from ROI
  1338. slices, height, width = plasticity_image.shape
  1339. # Create coordinate grid
  1340. y, x = np.meshgrid(np.arange(width), np.arange(height), indexing='xy')
  1341. points = np.vstack((x.ravel(), y.ravel())).T
  1342. # Check which points are inside polygon
  1343. path = MPLPath(polygon)
  1344. mask = path.contains_points(points).reshape(height, width)
  1345. # Expand mask to 3D (replicate for all Z slices)
  1346. expanded_mask = np.stack([mask] * slices, axis=0)
  1347. logger.info(f"ROI mask created: {np.sum(mask)} pixels, "
  1348. f"{slices} slices = {np.sum(expanded_mask)} total voxels")
  1349. # Apply mask to plasticity channel
  1350. masked_plasticity_image = np.copy(plasticity_image)
  1351. masked_plasticity_image[~expanded_mask] = 0
  1352. self.complexity_channel = masked_plasticity_image
  1353. # Update display
  1354. self.ui.graphWidget.setImage(masked_plasticity_image, autoLevels=True)
  1355. # Apply mask to fluorescence channel (if selected)
  1356. if fluo_channel_index == 0:
  1357. # "No channel" selected
  1358. fluo_image = None
  1359. elif 0 < fluo_channel_index <= len(self.current_image_data):
  1360. fluo_image = self.current_image_data[fluo_channel_index - 1]
  1361. else:
  1362. QMessageBox.warning(
  1363. self,
  1364. "Invalid Channel",
  1365. "Invalid fluorescence channel selected."
  1366. )
  1367. return
  1368. if fluo_image is not None:
  1369. masked_fluo_image = np.copy(fluo_image)
  1370. masked_fluo_image[~expanded_mask] = 0
  1371. self.fluor_channel = masked_fluo_image
  1372. logger.info("Fluorescence channel masked")
  1373. else:
  1374. self.fluor_channel = None
  1375. logger.info("No fluorescence channel selected")
  1376. logger.info(f"✓ ROI mask applied - Complexity channel: "
  1377. f"{np.count_nonzero(self.complexity_channel)} non-zero voxels, "
  1378. f"intensity: {np.sum(self.complexity_channel):.2f}")
  1379. # ====================================================================
  1380. # PROCESSING METHODS
  1381. # ====================================================================
  1382. def process(self):
  1383. """
  1384. Process the selected image to calculate structural plasticity metrics.
  1385. This is the main processing pipeline that:
  1386. 1. Validates parameters and ROI
  1387. 2. Extracts Z-range subset
  1388. 3. Applies ROI mask
  1389. 4. Initializes ImageProcessor
  1390. 5. Calculates PCA rotation and spreads
  1391. 6. Calculates fluorescence (if second channel)
  1392. 7. Plots distributions (optional)
  1393. 8. Saves results to CSV
  1394. 9. Updates UI
  1395. Workflow Steps
  1396. --------------
  1397. 1. Parameter validation
  1398. 2. Progress dialog creation
  1399. 3. Image preparation (ROI masking, Z-subset)
  1400. 4. Processor initialization
  1401. 5. PCA and spread calculation
  1402. 6. Fluorescence calculation (optional)
  1403. 7. Distribution plotting (optional)
  1404. 8. Results export to CSV
  1405. 9. UI update and success message
  1406. Notes
  1407. -----
  1408. - Shows progress dialog during processing
  1409. - Can be canceled by user at any time
  1410. - Errors are logged and displayed to user
  1411. - Processed images are marked in the list (green=success, red=error)
  1412. See Also
  1413. --------
  1414. ImageProcessor.process_image : Core processing algorithm
  1415. _validate_processing_parameters : Parameter validation
  1416. _prepare_masked_images : Image preparation
  1417. _save_results_to_csv : Results export
  1418. """
  1419. try:
  1420. logger.info("\n" + "="*70)
  1421. logger.info("STARTING PROCESSING")
  1422. logger.info("="*70)
  1423. # ============================================================
  1424. # STEP 1: VALIDATE PARAMETERS
  1425. # ============================================================
  1426. if not self._validate_processing_parameters():
  1427. return
  1428. # ============================================================
  1429. # STEP 2: GET PARAMETERS
  1430. # ============================================================
  1431. try:
  1432. voxel_size_x = float(self.ui.lineEdit_pixel_size_X.text())
  1433. voxel_size_y = float(self.ui.lineEdit_pixel_size_Y.text())
  1434. voxel_size_z = float(self.ui.lineEdit_pixel_size_Z.text())
  1435. z_start = self.ui.spinBox_zmin.value()
  1436. z_end = self.ui.spinBox_zmax.value()
  1437. except ValueError as e:
  1438. QMessageBox.critical(
  1439. self,
  1440. "Invalid Parameters",
  1441. f"Please check your numeric inputs:\n{str(e)}"
  1442. )
  1443. return
  1444. logger.info(f"Parameters: voxel_x={voxel_size_x:.3f}µm, "
  1445. f"voxel_y={voxel_size_y:.3f}µm, voxel_z={voxel_size_z:.3f}µm, "
  1446. f"z_range=[{z_start}, {z_end}]")
  1447. # Validate voxel sizes
  1448. is_valid, error_msg = validate_parameters(
  1449. voxel_size_x, voxel_size_y, voxel_size_z
  1450. )
  1451. if not is_valid:
  1452. QMessageBox.warning(self, "Invalid Voxel Sizes", error_msg)
  1453. return
  1454. # ============================================================
  1455. # STEP 3: CREATE PROGRESS DIALOG
  1456. # ============================================================
  1457. progress = QProgressDialog(
  1458. "Processing image...",
  1459. "Cancel",
  1460. 0, 100,
  1461. self
  1462. )
  1463. progress.setWindowModality(Qt.WindowModal)
  1464. progress.setWindowTitle("Structural Plasticity Analysis")
  1465. progress.setMinimumDuration(0) # Show immediately
  1466. progress.setValue(5)
  1467. # ============================================================
  1468. # STEP 4: PREPARE MASKED IMAGES
  1469. # ============================================================
  1470. progress.setLabelText("Applying ROI mask...")
  1471. progress.setValue(15)
  1472. masked_image_complexity, masked_image_fluor = self._prepare_masked_images(
  1473. z_start, z_end
  1474. )
  1475. if masked_image_complexity is None:
  1476. progress.close()
  1477. return
  1478. if progress.wasCanceled():
  1479. logger.info("Processing canceled by user")
  1480. progress.close()
  1481. return
  1482. progress.setValue(30)
  1483. # ============================================================
  1484. # STEP 5: INITIALIZE PROCESSOR
  1485. # ============================================================
  1486. progress.setLabelText("Initializing processor...")
  1487. processor = ImageProcessor(
  1488. voxel_size_x=voxel_size_x,
  1489. voxel_size_y=voxel_size_y,
  1490. voxel_size_z=voxel_size_z
  1491. )
  1492. progress.setValue(40)
  1493. # ============================================================
  1494. # STEP 6: PROCESS IMAGE (PCA + SPREADS)
  1495. # ============================================================
  1496. progress.setLabelText("Calculating PCA and spreads...")
  1497. logger.info(f"Processing complexity channel. Shape: {masked_image_complexity.shape}")
  1498. results = processor.process_image(
  1499. image_3d=masked_image_complexity,
  1500. mask_area_pixels=self.AArea
  1501. )
  1502. if progress.wasCanceled():
  1503. logger.info("Processing canceled by user")
  1504. progress.close()
  1505. return
  1506. progress.setValue(70)
  1507. # ============================================================
  1508. # STEP 7: CALCULATE FLUORESCENCE (if second channel exists)
  1509. # ============================================================
  1510. if masked_image_fluor is not None:
  1511. progress.setLabelText("Calculating fluorescence...")
  1512. logger.info("Calculating fluorescence for second channel")
  1513. fluor_px, fluor_um = processor.calculate_fluorescence(
  1514. masked_image_fluor,
  1515. self.AArea
  1516. )
  1517. results['fluorescence_px'] = fluor_px
  1518. results['fluorescence_um'] = fluor_um
  1519. progress.setValue(80)
  1520. # ============================================================
  1521. # STEP 8: PLOT DISTRIBUTIONS (if enabled)
  1522. # ============================================================
  1523. if self.ui.checkBox_show_distributions.isChecked():
  1524. self._plot_distributions(
  1525. results['MMsum'],
  1526. results['MMyy'],
  1527. results['MMzz']
  1528. )
  1529. # ============================================================
  1530. # STEP 9: SAVE RESULTS
  1531. # ============================================================
  1532. progress.setLabelText("Saving results...")
  1533. progress.setValue(90)
  1534. self._save_results_to_csv(results)
  1535. # ============================================================
  1536. # STEP 10: UPDATE UI
  1537. # ============================================================
  1538. self._update_ui_after_processing(success=True)
  1539. progress.setValue(100)
  1540. progress.close()
  1541. # ============================================================
  1542. # STEP 11: SHOW SUCCESS MESSAGE
  1543. # ============================================================
  1544. QMessageBox.information(
  1545. self,
  1546. "Processing Complete",
  1547. f"Image processed successfully!\n\n"
  1548. f"3D Spread: {results['spread_xyz_um']:.2f} µm³\n"
  1549. f"Axonal Volume: {results['axonal_volume']:.2f}\n"
  1550. f"Rotation Angle: {results['rotation_angle']:.2f}°"
  1551. )
  1552. logger.info("="*70)
  1553. logger.info("PROCESSING COMPLETED SUCCESSFULLY")
  1554. logger.info("="*70 + "\n")
  1555. except Exception as e:
  1556. logger.error(f"Processing error: {e}", exc_info=True)
  1557. if 'progress' in locals():
  1558. progress.close()
  1559. QMessageBox.critical(
  1560. self,
  1561. "Processing Error",
  1562. f"An error occurred during processing:\n\n{str(e)}\n\n"
  1563. f"Check the log file (plasticity_analyzer.log) for details."
  1564. )
  1565. self._update_ui_after_processing(success=False)
  1566. def _validate_processing_parameters(self) -> bool:
  1567. """
  1568. Validate all parameters before processing.
  1569. Checks:
  1570. - Image is loaded
  1571. - ROI is defined (minimum 3 vertices)
  1572. - ROI area is positive
  1573. - Z range is valid
  1574. - Voxel sizes are positive
  1575. Returns
  1576. -------
  1577. bool
  1578. True if all parameters are valid, False otherwise
  1579. Notes
  1580. -----
  1581. Displays warning dialogs for validation failures
  1582. """
  1583. # Check if image is loaded
  1584. if not hasattr(self, 'complexity_channel') or self.complexity_channel is None:
  1585. QMessageBox.warning(self, "No Image", "Please load an image first.")
  1586. return False
  1587. # Check if ROI is defined
  1588. if not hasattr(self, 'polygon_points') or len(self.polygon_points) < 3:
  1589. QMessageBox.warning(
  1590. self,
  1591. "No ROI",
  1592. "Please define a Region of Interest (ROI) first.\n"
  1593. "Click 'Select ROI' and draw a polygon around the structure."
  1594. )
  1595. return False
  1596. # Check if ROI area was calculated
  1597. if not hasattr(self, 'AArea') or self.AArea <= 0:
  1598. QMessageBox.warning(
  1599. self,
  1600. "Invalid ROI",
  1601. "ROI area is zero. Please redraw the ROI."
  1602. )
  1603. return False
  1604. # Validate Z range
  1605. z_start = self.ui.spinBox_zmin.value()
  1606. z_end = self.ui.spinBox_zmax.value()
  1607. if z_start >= z_end:
  1608. QMessageBox.warning(
  1609. self,
  1610. "Invalid Z Range",
  1611. f"Z start ({z_start}) must be less than Z end ({z_end})."
  1612. )
  1613. return False
  1614. if z_end > self.complexity_channel.shape[0]:
  1615. QMessageBox.warning(
  1616. self,
  1617. "Invalid Z Range",
  1618. f"Z end ({z_end}) exceeds image depth "
  1619. f"({self.complexity_channel.shape[0]})."
  1620. )
  1621. return False
  1622. # Validate voxel sizes
  1623. try:
  1624. voxel_x = float(self.ui.lineEdit_pixel_size_X.text())
  1625. voxel_y = float(self.ui.lineEdit_pixel_size_Y.text())
  1626. voxel_z = float(self.ui.lineEdit_pixel_size_Z.text())
  1627. if voxel_x <= 0 or voxel_y <= 0 or voxel_z <= 0:
  1628. QMessageBox.warning(
  1629. self,
  1630. "Invalid Voxel Size",
  1631. "All voxel sizes must be positive numbers."
  1632. )
  1633. return False
  1634. except ValueError:
  1635. QMessageBox.warning(
  1636. self,
  1637. "Invalid Voxel Size",
  1638. "Please enter valid numeric values for voxel sizes."
  1639. )
  1640. return False
  1641. logger.info("✓ All parameters validated")
  1642. return True
  1643. def _prepare_masked_images(
  1644. self,
  1645. z_start: int,
  1646. z_end: int
  1647. ) -> Tuple[Optional[np.ndarray], Optional[np.ndarray]]:
  1648. """
  1649. Prepare masked images for complexity and fluorescence channels.
  1650. This method:
  1651. 1. Extracts Z-slice subset
  1652. 2. Returns already-masked images (mask was applied in apply_roi_mask)
  1653. Parameters
  1654. ----------
  1655. z_start : int
  1656. Starting Z slice (inclusive)
  1657. z_end : int
  1658. Ending Z slice (inclusive)
  1659. Returns
  1660. -------
  1661. Tuple[Optional[np.ndarray], Optional[np.ndarray]]
  1662. (masked_complexity, masked_fluor)
  1663. - masked_complexity: Masked plasticity channel
  1664. - masked_fluor: Masked fluorescence channel (None if not selected)
  1665. Returns (None, None) if preparation fails
  1666. Notes
  1667. -----
  1668. - Images are already masked (apply_roi_mask was called earlier)
  1669. - Only Z-range extraction is performed here
  1670. - Logs intensity statistics for verification
  1671. """
  1672. try:
  1673. logger.info("Preparing masked images...")
  1674. # Extract Z-range from complexity channel
  1675. complexity_subset = self.complexity_channel[z_start:z_end+1, :, :]
  1676. logger.info(f"Complexity channel shape: {complexity_subset.shape}")
  1677. logger.info(f"Intensity: {np.sum(complexity_subset):.2f}, "
  1678. f"Non-zero: {np.count_nonzero(complexity_subset)}")
  1679. # Extract Z-range from fluorescence channel (if exists)
  1680. fluor_subset = None
  1681. if self.fluor_channel is not None:
  1682. fluor_subset = self.fluor_channel[z_start:z_end+1, :, :]
  1683. logger.info(f"Fluorescence channel shape: {fluor_subset.shape}")
  1684. return complexity_subset, fluor_subset
  1685. except Exception as e:
  1686. logger.error(f"Error preparing masked images: {e}", exc_info=True)
  1687. QMessageBox.critical(
  1688. self,
  1689. "Mask Error",
  1690. f"Failed to prepare images:\n{str(e)}"
  1691. )
  1692. return None, None
  1693. def _plot_distributions(
  1694. self,
  1695. MMsum: np.ndarray,
  1696. MMyy: np.ndarray,
  1697. MMzz: np.ndarray
  1698. ):
  1699. """
  1700. Plot X, Y, Z distributions for visual inspection.
  1701. Creates a 3-panel figure showing:
  1702. - Intensity distribution along X (horizontal)
  1703. - Y-spread at each X position
  1704. - Z-spread at each X position
  1705. Parameters
  1706. ----------
  1707. MMsum : np.ndarray
  1708. Intensity sum at each X position
  1709. MMyy : np.ndarray
  1710. Local Y-variance at each X position
  1711. MMzz : np.ndarray
  1712. Local Z-variance at each X position
  1713. Notes
  1714. -----
  1715. - Plots are shown in a matplotlib window
  1716. - User must close window to continue
  1717. - Useful for quality control and troubleshooting
  1718. """
  1719. try:
  1720. fig, axs = plt.subplots(1, 3, figsize=(15, 4))
  1721. # X distribution (intensity)
  1722. axs[0].plot(MMsum, linewidth=2, color='#2E86AB')
  1723. axs[0].set_title(
  1724. 'Intensity Along X (Horizontal)',
  1725. fontsize=14,
  1726. fontweight='bold'
  1727. )
  1728. axs[0].set_xlabel('X Position (pixels)', fontsize=12)
  1729. axs[0].set_ylabel('Intensity Sum', fontsize=12)
  1730. axs[0].grid(True, alpha=0.3)
  1731. axs[0].set_facecolor('#F8F9FA')
  1732. # Y distribution (variance)
  1733. axs[1].plot(MMyy, linewidth=2, color='#F18F01')
  1734. axs[1].set_title('Y-Spread at Each X', fontsize=14, fontweight='bold')
  1735. axs[1].set_xlabel('X Position (pixels)', fontsize=12)
  1736. axs[1].set_ylabel('Variance (pixels²)', fontsize=12)
  1737. axs[1].grid(True, alpha=0.3)
  1738. axs[1].set_facecolor('#F8F9FA')
  1739. # Z distribution (variance)
  1740. axs[2].plot(MMzz, linewidth=2, color='#06A77D')
  1741. axs[2].set_title('Z-Spread at Each X', fontsize=14, fontweight='bold')
  1742. axs[2].set_xlabel('X Position (pixels)', fontsize=12)
  1743. axs[2].set_ylabel('Variance (slices²)', fontsize=12)
  1744. axs[2].grid(True, alpha=0.3)
  1745. axs[2].set_facecolor('#F8F9FA')
  1746. plt.suptitle('Distribution Analysis', fontsize=16, fontweight='bold', y=1.02)
  1747. plt.tight_layout()
  1748. plt.show()
  1749. logger.info("Distribution plots displayed")
  1750. except Exception as e:
  1751. logger.warning(f"Failed to plot distributions: {e}")
  1752. def _save_results_to_csv(self, results: dict):
  1753. """
  1754. Save processing results to CSV file.
  1755. Appends a new row with:
  1756. - Image filename
  1757. - Spread metrics (pixels and µm)
  1758. - Axonal volume
  1759. - Fluorescence values
  1760. - User observation/notes
  1761. Parameters
  1762. ----------
  1763. results : dict
  1764. Processing results from ImageProcessor
  1765. Notes
  1766. -----
  1767. - CSV file path was set during load_images()
  1768. - Observation text is taken from UI text edit widget
  1769. - Results are appended to existing CSV (does not overwrite)
  1770. """
  1771. try:
  1772. if not hasattr(self, 'csv_file_path') or not self.csv_file_path:
  1773. logger.error("No CSV file path defined")
  1774. QMessageBox.warning(
  1775. self,
  1776. "No Output File",
  1777. "Output CSV file is not defined."
  1778. )
  1779. return
  1780. observation = self.ui.textEdit_observation.toPlainText()
  1781. image_name = os.path.basename(self.current_image_filepath)
  1782. # Write to CSV
  1783. with open(self.csv_file_path, mode='a', newline='', encoding='utf-8') as csv_file:
  1784. writer = csv.writer(csv_file)
  1785. writer.writerow([
  1786. image_name,
  1787. results['spread_x_pixel'],
  1788. results['spread_y_pixel'],
  1789. results['spread_z_pixel'],
  1790. results['spread_xy_pixel'],
  1791. results['spread_xyz_pixel'],
  1792. results['spread_x_um'],
  1793. results['spread_y_um'],
  1794. results['spread_z_um'],
  1795. results['spread_xy_um'],
  1796. results['spread_xyz_um'],
  1797. results['axonal_volume'],
  1798. results['fluorescence_px'],
  1799. results['fluorescence_um'],
  1800. f'"{observation}"'
  1801. ])
  1802. logger.info(f"✓ Results saved to: {self.csv_file_path}")
  1803. except Exception as e:
  1804. logger.error(f"Error saving results: {e}", exc_info=True)
  1805. QMessageBox.critical(
  1806. self,
  1807. "Save Error",
  1808. f"Failed to save results to CSV:\n{str(e)}"
  1809. )
  1810. def _update_ui_after_processing(self, success: bool = True):
  1811. """
  1812. Update UI elements after processing.
  1813. Parameters
  1814. ----------
  1815. success : bool, optional
  1816. Whether processing was successful (default: True)
  1817. UI Updates
  1818. ----------
  1819. - Mark processed image in list:
  1820. - Green background: Success
  1821. - Red background: Error
  1822. - Update status bar (if available)
  1823. Notes
  1824. -----
  1825. Errors in UI update are logged but do not interrupt workflow
  1826. """
  1827. try:
  1828. # Mark processed image in list
  1829. selected_items = self.ui.listWidget_images.selectedItems()
  1830. if selected_items:
  1831. if success:
  1832. selected_items[0].setBackground(pyqtgraph.mkColor('lightgreen'))
  1833. else:
  1834. selected_items[0].setBackground(pyqtgraph.mkColor("#FB889A"))
  1835. # Update status bar if exists
  1836. if hasattr(self, 'statusBar'):
  1837. if success:
  1838. self.statusBar().showMessage("✓ Processing completed", 5000)
  1839. else:
  1840. self.statusBar().showMessage("✗ Processing failed", 5000)
  1841. except Exception as e:
  1842. logger.warning(f"Failed to update UI: {e}")
  1843. # ========================================================================
  1844. # APPLICATION ENTRY POINT
  1845. # ========================================================================
  1846. if __name__ == "__main__":
  1847. app = QApplication(sys.argv)
  1848. window = MyMainWindow()
  1849. window.show()
  1850. sys.exit(app.exec())

MorphoScope.py at commit 6cb847b, under MIT · at the source

Overview

Authors: Athira Theyyassanchery Mani1, Vivian Backs1, Christian Werner2, Charlotte Helfrich-Förster3, Thomas Raabe1
  1. Molecular Genetics, Biocenter, University of Würzburg, Würzburg, Germany
  2. Biotechnology and Biophysics, Biocenter, University of Würzburg, Würzburg, Germany
  3. Neurobiology and Genetics, Biocenter, University of Würzburg, Würzburg, Germany
Institutions: University of Würzburg (Germany)
Journal: Journal of biological rhythms, volume 41, issue 4, pages 416-434
Dates: published online 12 April 2026; in print August 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1177/07487304261434715 · PMID 41966995 · PMCID PMC13342480 · OpenAlex W7153884631
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: drosophila (organism), cellular / molecular (subfield)
Methods: Statistics, Evoked potentials, fMRI & imaging
Keywords: Drosophila, clock neurons, structural plasticity, circadian rhythm, adaptation
MeSH: Behavior, Animal*, Circadian Rhythm*, Drosophila*, Drosophila melanogaster*, Drosophila Proteins*, Neuronal Plasticity*, Neurons*, Ribosomal Protein S6 Kinases*, Ribosomal Protein S6 Kinases, 70-kDa*, Animals, Mutation, Ribosomal Protein S6 Kinases, 90-kDa (* major topic)
Topic: Circadian rhythm and melatonin (Endocrine and Autonomic Systems, Neuroscience), according to OpenAlex
Funding: Deutsche Forschungsgemeinschaft (Fo207-19/1, Ra561-11/1)
Citations: cited by 2 papers (Europe PMC); 83 references in the paper

Abstract

Circadian neuronal plasticity describes daily recurring changes at the level of neuronal morphology, connectivity and synaptic processes. Disturbance of these plastic changes could result in inflexibility of an organism to adapt behavior to changing environmental cues. The mitogen activated protein kinases (MAPK)/ERK signaling pathway is involved both in circadian processes and neuronal plasticity. Ribosomal S6 kinases (RSK) act as downstream mediators of ERK signaling with apparently pleiotropic—but sometimes poorly understood- functions in the nervous system. This is illustrated by some major gaps in our understanding of the pathophysiological processes caused by RSK2 mutations in humans that lead to intellectual disabilities. Previous studies described the role of Drosophila RSK as one regulator of the molecular circadian oscillator. Here we could show that RSK kinase activity is required to control another aspect of circadian rhythmicity, the daily remodeling of the dorsal branching pattern of the small ventral lateral neurons (s-LNv) as the central pacemaker cells. Loss of RSK function resulted in more fasciculated and less branched s-LNv’s in the early morning, which could affect synaptic in- or output connectivity. Increased fasciculation correlated with a reduced number of Bruchpilot sites as a marker for presynapses. Analysis of the expression of the Pigment Dispersing Factor PDF in s-LNv’s, the most important signaling factor between clock neurons, revealed no evidence of changes in RSK mutants. Consistent with unaffected PDF signaling as a major output from the s-LNv’s, RSK mutant flies are rhythmic. Their free-running rhythms show even a significantly higher power than those of the wild-type controls. This robustness is at the expense of flexibility to adapt their activity to variations in light conditions. Together with the known role of RSK in olfactory learning and memory processes our results suggest that RSK is required to maintain experience dependent plasticity.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 1 match between paragraphs and lines of code.

FranTassara/MorphoScope

License: MIT
State: the link answers, verified on 26 September 2026
Evidence: files inventoried
Commit: 6cb847ba76bdaeab3d8ba2b247e8ce269cde485c, 16 July 2026
Languages: Python (4)
Size: 15 files, 4 scripts
Software Heritage: not archived
Found in: the text, “Quantification of s-LN v Arborization”
Holds: README, license file, CITATION.cff, environment (requirements.txt)
Not found: tests, continuous integration, documentation
Tools: NumPy (3 files), Matplotlib (2 files), SciPy (2 files), tifffile (2 files), scikit-image (1 file)
Availability: 1 check, the latest on 26 September 2026: the link answers
  • 26 September 2026: the link answers
6 files

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 4 scripts, each with its path and the digest of its content;
  • 1 match between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 29 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 5 authors, 5 keywords, 12 MeSH terms, 1 funder, 83 references.

Cite

This paper

Theyyassanchery Mani, A., Backs, V., Werner, C., Helfrich-Förster, C., & Raabe, T. (2026). &lt;i&gt;Drosophila&lt;/i&gt; RSK: A Pivotal Regulator of Circadian Plasticity at the Neuronal and Behavioral Level. Journal of biological rhythms, 41(4), 416-434. https://doi.org/10.1177/07487304261434715

BibTeX

@article{theyyassancherymani2026lt,
author = {Theyyassanchery Mani, Athira and Backs, Vivian and Werner, Christian and Helfrich-Förster, Charlotte and Raabe, Thomas},
title = {{\&lt;i\&gt;Drosophila\&lt;/i\&gt; RSK: A Pivotal Regulator of Circadian Plasticity at the Neuronal and Behavioral Level}},
journal = {Journal of biological rhythms},
year = {2026},
month = apr,
volume = {41},
number = {4},
pages = {416--434},
publisher = {SAGE Publishing},
issn = {0748-7304},
doi = {10.1177/07487304261434715},
url = {https://doi.org/10.1177/07487304261434715},
pmid = {41966995},
pmcid = {PMC13342480}
}

RIS

TY - JOUR
AU - Theyyassanchery Mani, Athira
AU - Backs, Vivian
AU - Werner, Christian
AU - Helfrich-Förster, Charlotte
AU - Raabe, Thomas
TI - &lt;i&gt;Drosophila&lt;/i&gt; RSK: A Pivotal Regulator of Circadian Plasticity at the Neuronal and Behavioral Level
T2 - Journal of biological rhythms
J2 - J Biol Rhythms
PY - 2026
DA - 2026/04/12
VL - 41
IS - 4
SP - 416
EP - 434
SN - 0748-7304
PB - SAGE Publishing
DO - 10.1177/07487304261434715
UR - https://doi.org/10.1177/07487304261434715
LA - en
ER -

CSL-JSON

{
"id": "10.1177/07487304261434715",
"type": "article-journal",
"title": "&lt;i&gt;Drosophila&lt;/i&gt; RSK: A Pivotal Regulator of Circadian Plasticity at the Neuronal and Behavioral Level",
"container-title": "Journal of biological rhythms",
"author": [
{
"family": "Theyyassanchery Mani",
"given": "Athira"
},
{
"family": "Backs",
"given": "Vivian"
},
{
"family": "Werner",
"given": "Christian"
},
{
"family": "Helfrich-Förster",
"given": "Charlotte"
},
{
"family": "Raabe",
"given": "Thomas"
}
],
"container-title-short": "J Biol Rhythms",
"volume": "41",
"issue": "4",
"page": "416-434",
"DOI": "10.1177/07487304261434715",
"PMID": "41966995",
"PMCID": "PMC13342480",
"ISSN": "0748-7304",
"publisher": "SAGE Publishing",
"URL": "https://doi.org/10.1177/07487304261434715",
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
12
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1098/rsob.260120 [code]
Assessment of adult structural plasticity in Drosophila neurons.
Journal: Open biology
In common: tifffile, scikit-image, SciPy, 2 other tools, drosophila, cellular / molecular, 9 references
[2] doi:10.1016/j.isci.2026.117125
Brain circadian clock neurons drive fitness advantages in &lt;i&gt;Drosophila&lt;/i&gt;.
Journal: iScience
In common: drosophila, 10 references
[3] doi:10.7554/elife.103359
Circadian control of a sex-specific behavior in &lt;i&gt;Drosophila&lt;/i&gt;.
Journal: eLife
In common: drosophila, 8 references
[4] doi:10.1038/s41586-026-10735-w [code]
Distributed control circuits across a brain-and-cord connectome.
Journal: Nature
In common: scikit-image, SciPy, Matplotlib, 1 other tool, drosophila, 3 references
[5] doi:10.1172/jci193475
Conserved sleep disturbances in FOXP1 syndrome originate from developmental dysregulation of peptidergic signaling.
Journal: The Journal of clinical investigation
In common: drosophila, 4 references
[6] doi:10.3389/fnsys.2026.1822122 [code]
Convergence-divergence circuits for multimodal integration of innate and learned opponent valences.
Journal: Frontiers in systems neuroscience
In common: tifffile, scikit-image, SciPy, 2 other tools, 1 reference
[7] doi:10.7554/elife.107276 [code]
Deployment of endocytic machinery to periactive zones of nerve terminals is independent of active zone assembly and evoked release.
Journal: eLife
In common: tifffile, scikit-image, SciPy, 2 other tools, drosophila, cellular / molecular
[8] doi:10.7554/elife.108021 [code]
BetaII-spectrin gaps and patches emerge from the patterned assembly of the actin/spectrin membrane skeleton in human motor neuron axons.
Journal: eLife
In common: tifffile, scikit-image, SciPy, 2 other tools, cellular / molecular, 1 reference
[9] doi:10.1016/j.isci.2026.117010 [code]
Deep learning-assisted mapping of dendritic spines using sequential 2D two-photon calcium imaging.
Journal: iScience
In common: tifffile, scikit-image, SciPy, 2 other tools, cellular / molecular, 1 reference
[10] doi:10.1016/j.isci.2026.116213 [code]
Opioid receptor distribution in the claustrum-dorsal endopiriform complex.
Journal: iScience
In common: tifffile, scikit-image, SciPy, 2 other tools, cellular / molecular, 1 reference

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.