tomwer 1.3.0.dev2__py3-none-any.whl → 1.3.0rc10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. orangecontrib/tomwer/widgets/__init__.py +11 -12
  2. orangecontrib/tomwer/widgets/control/DataListenerOW.py +6 -6
  3. orangecontrib/tomwer/widgets/control/DataValidatorOW.py +6 -6
  4. orangecontrib/tomwer/widgets/control/NXTomomillMixIn.py +3 -3
  5. orangecontrib/tomwer/widgets/control/NXTomomillOW.py +10 -8
  6. orangecontrib/tomwer/widgets/control/SingleTomoObjOW.py +6 -6
  7. orangecontrib/tomwer/widgets/debugtools/DatasetGeneratorOW.py +1 -1
  8. orangecontrib/tomwer/widgets/icat/RawDataScreenshotCreatorOW.py +98 -98
  9. orangecontrib/tomwer/widgets/icat/SaveToGalleryAndPublishOW.py +129 -129
  10. orangecontrib/tomwer/widgets/reconstruction/AxisOW.py +13 -12
  11. orangecontrib/tomwer/widgets/reconstruction/SAAxisOW.py +11 -9
  12. orangecontrib/tomwer/widgets/reconstruction/SADeltaBetaOW.py +11 -9
  13. orangecontrib/tomwer/widgets/reconstruction/SinoNormOW.py +12 -15
  14. orangecontrib/tomwer/widgets/visualization/DataViewerOW.py +9 -9
  15. orangecontrib/tomwer/widgets/visualization/DiffViewerOW.py +1 -1
  16. orangecontrib/tomwer/widgets/visualization/SinogramViewerOW.py +0 -1
  17. tomwer/__main__.py +0 -10
  18. tomwer/app/canvas_launcher/config.py +3 -3
  19. tomwer/app/canvas_launcher/environ.py +1 -0
  20. tomwer/app/intensitynormalization.py +12 -11
  21. tomwer/app/nabuapp.py +0 -11
  22. tomwer/app/zstitching.py +11 -1
  23. tomwer/core/process/control/datalistener/datalistener.py +15 -10
  24. tomwer/core/process/control/nxtomomill.py +1 -1
  25. tomwer/core/process/control/scantransfer.py +8 -32
  26. tomwer/core/process/edit/darkflatpatch.py +8 -9
  27. tomwer/core/process/edit/imagekeyeditor.py +20 -22
  28. tomwer/core/process/icat/screenshots.py +1 -0
  29. tomwer/core/process/reconstruction/axis/axis.py +263 -59
  30. tomwer/core/process/reconstruction/axis/mode.py +161 -50
  31. tomwer/core/process/reconstruction/axis/params.py +23 -20
  32. tomwer/core/process/reconstruction/darkref/darkrefs.py +12 -13
  33. tomwer/core/process/reconstruction/nabu/castvolume.py +3 -3
  34. tomwer/core/process/reconstruction/nabu/nabucommon.py +43 -19
  35. tomwer/core/process/reconstruction/nabu/nabuscores.py +34 -7
  36. tomwer/core/process/reconstruction/nabu/nabuslices.py +81 -26
  37. tomwer/core/process/reconstruction/nabu/nabuvolume.py +31 -26
  38. tomwer/core/process/reconstruction/nabu/plane.py +9 -0
  39. tomwer/core/process/reconstruction/nabu/utils.py +32 -9
  40. tomwer/core/process/reconstruction/saaxis/saaxis.py +4 -1
  41. tomwer/core/process/reconstruction/sadeltabeta/sadeltabeta.py +9 -1
  42. tomwer/core/process/reconstruction/scores/params.py +3 -3
  43. tomwer/core/process/reconstruction/test/test_darkref_copy.py +4 -4
  44. tomwer/core/process/stitching/nabustitcher.py +11 -10
  45. tomwer/core/process/task.py +33 -27
  46. tomwer/core/process/test/test_axis.py +7 -6
  47. tomwer/core/process/test/test_data_transfer.py +3 -3
  48. tomwer/core/process/test/test_nabu.py +10 -2
  49. tomwer/core/process/test/test_normalization.py +2 -2
  50. tomwer/core/scan/blissscan.py +3 -3
  51. tomwer/core/scan/edfscan.py +9 -9
  52. tomwer/core/scan/nxtomoscan.py +11 -11
  53. tomwer/core/scan/scanbase.py +31 -24
  54. tomwer/core/scan/test/test_future_scan.py +1 -1
  55. tomwer/core/scan/test/test_h5.py +4 -4
  56. tomwer/core/scan/test/test_process_registration.py +2 -2
  57. tomwer/core/scan/test/test_scan.py +1 -75
  58. tomwer/core/settings.py +3 -3
  59. tomwer/core/test/test_utils.py +2 -2
  60. tomwer/core/volume/edfvolume.py +6 -6
  61. tomwer/core/volume/hdf5volume.py +6 -6
  62. tomwer/core/volume/jp2kvolume.py +6 -6
  63. tomwer/core/volume/rawvolume.py +6 -6
  64. tomwer/core/volume/tiffvolume.py +12 -12
  65. tomwer/gui/cluster/slurm.py +14 -9
  66. tomwer/gui/cluster/supervisor.py +12 -0
  67. tomwer/gui/cluster/test/test_cluster.py +1 -2
  68. tomwer/gui/cluster/test/test_supervisor.py +1 -1
  69. tomwer/gui/control/datalist.py +5 -0
  70. tomwer/gui/control/datawatcher/controlwidget.py +2 -4
  71. tomwer/gui/control/reducedarkflatselector.py +8 -8
  72. tomwer/gui/control/test/test_single_tomo_obj.py +1 -1
  73. tomwer/gui/edit/dkrfpatch.py +4 -4
  74. tomwer/gui/edit/nxtomowarmer.py +2 -2
  75. tomwer/gui/edit/test/test_dkrf_patch.py +6 -6
  76. tomwer/gui/imagefromfile.py +2 -2
  77. tomwer/gui/qfolderdialog.py +5 -0
  78. tomwer/gui/reconstruction/axis/CompareImages.py +94 -168
  79. tomwer/gui/reconstruction/axis/radioaxis.py +58 -182
  80. tomwer/gui/reconstruction/darkref/darkrefwidget.py +2 -1
  81. tomwer/gui/reconstruction/nabu/castvolume.py +8 -1
  82. tomwer/gui/reconstruction/nabu/nabuconfig/reconstruction.py +54 -21
  83. tomwer/gui/reconstruction/normalization/intensity.py +3 -25
  84. tomwer/gui/reconstruction/saaxis/corrangeselector.py +1 -1
  85. tomwer/gui/reconstruction/saaxis/saaxis.py +1 -11
  86. tomwer/gui/reconstruction/sadeltabeta/saadeltabeta.py +0 -10
  87. tomwer/gui/reconstruction/scores/scoreplot.py +1 -6
  88. tomwer/gui/reconstruction/test/test_axis.py +18 -4
  89. tomwer/gui/reconstruction/test/test_nabu.py +3 -0
  90. tomwer/gui/stitching/stitching.py +2 -2
  91. tomwer/gui/stitching/stitching_preview.py +7 -53
  92. tomwer/gui/stitching/stitching_raw.py +3 -3
  93. tomwer/gui/utils/inputwidget.py +12 -2
  94. tomwer/gui/utils/lineselector/lineselector.py +1 -1
  95. tomwer/gui/visualization/dataviewer.py +47 -17
  96. tomwer/gui/visualization/sinogramviewer.py +19 -26
  97. tomwer/gui/visualization/test/test_volumeviewer.py +64 -66
  98. tomwer/gui/visualization/volumeviewer.py +105 -105
  99. tomwer/io/utils/h5pyutils.py +7 -3
  100. tomwer/io/utils/utils.py +3 -3
  101. tomwer/resources/gui/icons/parameters.svg +1 -1
  102. tomwer/resources/gui/illustrations/no_rot.svg +1 -1
  103. tomwer/synctools/stacks/edit/darkflatpatch.py +17 -12
  104. tomwer/tests/test_scripts.py +0 -3
  105. tomwer/third_part/WaitingOverlay.py +110 -0
  106. tomwer/third_part/__init__.py +0 -0
  107. tomwer/version.py +2 -2
  108. {tomwer-1.3.0.dev2.dist-info → tomwer-1.3.0rc10.dist-info}/METADATA +32 -31
  109. {tomwer-1.3.0.dev2.dist-info → tomwer-1.3.0rc10.dist-info}/RECORD +115 -153
  110. {tomwer-1.3.0.dev2.dist-info → tomwer-1.3.0rc10.dist-info}/WHEEL +1 -1
  111. orangecontrib/tomwer/widgets/reconstruction/TofuOW.py +0 -197
  112. orangecontrib/tomwer/widgets/reconstruction/icons/XY_lamino.svg +0 -168
  113. orangecontrib/tomwer/widgets/reconstruction/icons/XZ_lamino.svg +0 -275
  114. orangecontrib/tomwer/widgets/reconstruction/icons/YZ_lamino.svg +0 -182
  115. tomwer/app/lamino.py +0 -143
  116. tomwer/core/process/reconstruction/lamino/__init__.py +0 -1
  117. tomwer/core/process/reconstruction/lamino/tofu.py +0 -1000
  118. tomwer/core/process/test/test_lamino.py +0 -76
  119. tomwer/core/test/test_lamino.py +0 -92
  120. tomwer/gui/reconstruction/lamino/__init__.py +0 -31
  121. tomwer/gui/reconstruction/lamino/tofu/TofuOptionLoader.py +0 -107
  122. tomwer/gui/reconstruction/lamino/tofu/__init__.py +0 -1
  123. tomwer/gui/reconstruction/lamino/tofu/misc.py +0 -148
  124. tomwer/gui/reconstruction/lamino/tofu/projections.py +0 -896
  125. tomwer/gui/reconstruction/lamino/tofu/settings.py +0 -75
  126. tomwer/gui/reconstruction/lamino/tofu/tofu.py +0 -432
  127. tomwer/gui/reconstruction/lamino/tofu/tofuexpert.py +0 -567
  128. tomwer/gui/reconstruction/lamino/tofu/tofuoutput.py +0 -757
  129. tomwer/gui/reconstruction/test/test_lamino.py +0 -194
  130. tomwer/resources/gui/icons/lamino_parameters.svg +0 -70
  131. tomwer/resources/gui/illustrations/lamino_angle.png +0 -0
  132. tomwer/resources/gui/illustrations/lamino_angle.svg +0 -509
  133. tomwer/resources/gui/illustrations/lamino_beta_angle.png +0 -0
  134. tomwer/resources/gui/illustrations/lamino_beta_angle.svg +0 -97
  135. tomwer/resources/gui/illustrations/lamino_theta_angle.png +0 -0
  136. tomwer/resources/gui/illustrations/lamino_theta_angle.svg +0 -368
  137. tomwer/resources/gui/illustrations/manual_slice.png +0 -0
  138. tomwer/resources/gui/illustrations/manual_slice.svg +0 -221
  139. tomwer/resources/gui/illustrations/psi_angle.png +0 -0
  140. tomwer/resources/gui/illustrations/psi_angle.svg +0 -479
  141. tomwer/resources/gui/illustrations/rotation_center.png +0 -0
  142. tomwer/resources/gui/illustrations/rotation_center.svg +0 -276
  143. tomwer/resources/gui/illustrations/slice_stack.png +0 -0
  144. tomwer/resources/gui/illustrations/slice_stack.svg +0 -266
  145. tomwer/resources/gui/illustrations/xy_slice.png +0 -0
  146. tomwer/resources/gui/illustrations/xy_slice.svg +0 -269
  147. tomwer/resources/gui/illustrations/xz_slice.png +0 -0
  148. tomwer/resources/gui/illustrations/xz_slice.svg +0 -270
  149. tomwer/resources/gui/illustrations/yz_slice.png +0 -0
  150. tomwer/resources/gui/illustrations/yz_slice.svg +0 -270
  151. tomwer/synctools/stacks/reconstruction/lamino.py +0 -233
  152. /tomwer-1.3.0.dev2-py3.11-nspkg.pth → /tomwer-1.3.0rc10-py3.11-nspkg.pth +0 -0
  153. {tomwer-1.3.0.dev2.dist-info → tomwer-1.3.0rc10.dist-info}/LICENSE +0 -0
  154. {tomwer-1.3.0.dev2.dist-info → tomwer-1.3.0rc10.dist-info}/entry_points.txt +0 -0
  155. {tomwer-1.3.0.dev2.dist-info → tomwer-1.3.0rc10.dist-info}/namespace_packages.txt +0 -0
  156. {tomwer-1.3.0.dev2.dist-info → tomwer-1.3.0rc10.dist-info}/top_level.txt +0 -0
@@ -31,6 +31,7 @@ import logging
31
31
  import weakref
32
32
 
33
33
  from silx.gui import qt
34
+ from silx.gui.dialog.ColormapDialog import DisplayMode
34
35
  from silx.gui.plot.ImageStack import ImageStack as _ImageStack
35
36
  from silx.gui.plot.ImageStack import UrlLoader
36
37
  from silx.gui.utils.signal import SignalProxy
@@ -79,6 +80,7 @@ class DataViewer(qt.QMainWindow):
79
80
  self._viewer = ImageStack(
80
81
  parent=self, show_overview=show_overview, backend=backend
81
82
  )
83
+ self._viewer.getPlotWidget().getMaskAction().setVisible(False)
82
84
  self._viewer.getPlotWidget().setYAxisInverted(Y_AXIS_DOWNWARD)
83
85
  # set an UrlLoader managing .npy and .vol
84
86
  self._viewer.getPlotWidget().setKeepDataAspectRatio(True)
@@ -123,9 +125,6 @@ class DataViewer(qt.QMainWindow):
123
125
  def setScanOverviewVisible(self, visible: bool) -> None:
124
126
  self._viewer.setScanOverviewVisible(visible=visible)
125
127
 
126
- def cleanBeforeQuit(self):
127
- self._viewer._plot.updateThread.stop()
128
-
129
128
  def getUrlListDockWidget(self):
130
129
  return self._viewer.getUrlListDockWidget()
131
130
 
@@ -412,14 +411,27 @@ class DisplayControl(qt.QWidget):
412
411
 
413
412
 
414
413
  class ImageStack(_ImageStack):
414
+ """
415
+ Image stack dedicated to data display.
416
+
417
+ It deal for example with data normalization...
418
+ """
419
+
415
420
  def __init__(self, parent, show_overview=True, backend=None):
416
421
  self._normFct = None
417
422
  self._url_indexes = None
418
423
  super().__init__(parent)
419
424
  self.getPlotWidget().setBackend(backend)
425
+
426
+ # tune colormap dialog to have histogram by default
427
+ colormapAction = self.getPlotWidget().getColormapAction()
428
+ colormapDialog = colormapAction.getColormapDialog()
429
+ colormapDialog.getHistogramWidget().setDisplayMode(DisplayMode.HISTOGRAM)
430
+ colormapAction.setColormapDialog(colormapDialog)
431
+
420
432
  self.setUrlLoaderClass(_TomwerUrlLoader)
421
433
  # hide axis to be display
422
- self._plot.getPlotWidget().setAxesDisplayed(False)
434
+ self._plot.setAxesDisplayed(False)
423
435
  self._loadSliceParams = False
424
436
  self._resetZoom = True
425
437
 
@@ -484,17 +496,15 @@ class ImageStack(_ImageStack):
484
496
  return self._tableDockWidget
485
497
 
486
498
  def resetZoom(self):
487
- self._plot.getPlotWidget().resetZoom()
499
+ self._plot.resetZoom()
488
500
 
489
501
  def setLimits(self, x_min, x_max, y_min, y_max):
490
- plot = self._plot.getPlotWidget()
491
- plot.setLimits(xmin=x_min, ymin=y_min, xmax=x_max, ymax=y_max)
502
+ self._plot.setLimits(xmin=x_min, ymin=y_min, xmax=x_max, ymax=y_max)
492
503
 
493
504
  def getLimits(self):
494
- plot = self._plot.getPlotWidget()
495
505
  limits = []
496
- limits.extend(plot.getGraphXLimits())
497
- limits.extend(plot.getGraphYLimits())
506
+ limits.extend(self._plot.getGraphXLimits())
507
+ limits.extend(self._plot.getGraphYLimits())
498
508
  return tuple(limits)
499
509
 
500
510
  def setSliceReconsParamsVisible(self, visible):
@@ -526,8 +536,19 @@ class ImageStack(_ImageStack):
526
536
  return
527
537
 
528
538
  if data.ndim != 2:
529
- if data.ndim == 3 and data.shape[0] == 1:
530
- data = data.reshape((data.shape[1], data.shape[2]))
539
+ if data.ndim == 3:
540
+ if data.shape[0] == 1:
541
+ # if reconstruction along z
542
+ data = data.reshape((data.shape[1], data.shape[2]))
543
+ elif data.shape[1] == 1:
544
+ # if reconstruction along y
545
+ data = data.reshape((data.shape[0], data.shape[2]))
546
+ elif data.shape[2] == 1:
547
+ # if reconstruction along z
548
+ data = data.reshape((data.shape[0], data.shape[1]))
549
+ else:
550
+ _logger.warning(f"Image Stack only manage 2D data. Url: {url}")
551
+ return
531
552
  else:
532
553
  _logger.warning(f"Image Stack only manage 2D data. Url: {url}")
533
554
  return
@@ -538,10 +559,14 @@ class ImageStack(_ImageStack):
538
559
  self._urlData[url] = norm_data
539
560
 
540
561
  if self.getCurrentUrl().path() == url:
541
- self._plot.setData(self._urlData[url])
562
+ self._plot.addImage(self._urlData[url])
563
+ if hasattr(self, "getWaiterOverlay"):
564
+ self.getWaiterOverlay().hide()
565
+ else:
566
+ self._waitingOverlay.hide()
542
567
  if self._resetZoom:
543
568
  self._resetZoom = False
544
- self._plot.getPlotWidget().resetZoom()
569
+ self._plot.resetZoom()
545
570
 
546
571
  if sender in self._loadingThreads:
547
572
  self._loadingThreads.remove(sender)
@@ -551,11 +576,16 @@ class ImageStack(_ImageStack):
551
576
  self._resetZoom = reset
552
577
 
553
578
  def setCurrentUrl(self, url: DataUrl):
554
- if isinstance(url, str):
579
+ if url in ("", None):
580
+ url = None
581
+ elif isinstance(url, str):
555
582
  url = DataUrl(path=url)
556
583
  elif not isinstance(url, DataUrl):
557
584
  raise TypeError
558
- if self._loadSliceParams:
585
+ if url is None:
586
+ pass
587
+ # FIXME: add a function to clear the metadata widget
588
+ elif self._loadSliceParams:
559
589
  try:
560
590
  self._reconsWidget.setVolumeMetadata(
561
591
  self._metadatas.get(url.path(), None)
@@ -566,7 +596,7 @@ class ImageStack(_ImageStack):
566
596
 
567
597
  def setUrls(self, urls: list):
568
598
  _ImageStack.setUrls(self, urls)
569
- listWidget = self._urlsTable._urlsTable._listWidget
599
+ listWidget = self._urlsTable._urlsTable
570
600
  items = []
571
601
  for i in range(listWidget.count()):
572
602
  # TODO: do this on the fly
@@ -35,14 +35,8 @@ import logging
35
35
  import weakref
36
36
 
37
37
  from silx.gui import qt
38
-
39
- try:
40
- from silx.gui.plot.ImageStack import ( # noqa F401
41
- PlotWithWaitingLabel as _PlotWithWaitingLabel,
42
- )
43
- except ImportError:
44
- from silx.gui.plot.ImageStack import _PlotWithWaitingLabel # noqa F401
45
-
38
+ from silx.gui.plot import Plot2D
39
+ from silx.gui.widgets.WaitingOverlay import WaitingOverlay
46
40
  from tomwer.gui.settings import Y_AXIS_DOWNWARD
47
41
 
48
42
  _logger = logging.getLogger(__name__)
@@ -72,13 +66,16 @@ class SinogramViewer(qt.QMainWindow):
72
66
  # used to memorize sinogram properties when load it.
73
67
  # Contains (str(scan), line, oversampling)
74
68
 
75
- self._plot = _PlotWithWaitingLabel(parent=self)
76
- self._plot.getPlotWidget().setYAxisInverted(Y_AXIS_DOWNWARD)
77
- plot = self._plot.getPlotWidget()
78
- plot.getDefaultColormap().setVRange(None, None)
79
- plot.setAxesDisplayed(False)
80
- plot.setKeepDataAspectRatio(True)
81
- self._plot.setWaiting(False)
69
+ self._plot = Plot2D(parent=self)
70
+ self._plotWaiter = WaitingOverlay(self._plot)
71
+ self._plotWaiter.hide()
72
+ self._plotWaiter.setIconSize(qt.QSize(30, 30))
73
+
74
+ self._plot.getMaskAction().setVisible(False)
75
+ self._plot.setYAxisInverted(Y_AXIS_DOWNWARD)
76
+ self._plot.getDefaultColormap().setVRange(None, None)
77
+ self._plot.setAxesDisplayed(False)
78
+ self._plot.setKeepDataAspectRatio(True)
82
79
  self._dockOpt = qt.QDockWidget(self)
83
80
  self._options = SinogramOpts(parent=self, orientation=opts_orientation)
84
81
  self._dockOpt.setWidget(self._options)
@@ -95,7 +92,7 @@ class SinogramViewer(qt.QMainWindow):
95
92
  self._loadingThread.finished.connect(self._sinogram_loaded)
96
93
 
97
94
  # expose API
98
- self.getActiveImage = plot.getActiveImage
95
+ self.getActiveImage = self._plot.getActiveImage
99
96
 
100
97
  # set up
101
98
  if scan is not None:
@@ -131,12 +128,12 @@ class SinogramViewer(qt.QMainWindow):
131
128
  self._options.setSubsampling(value)
132
129
 
133
130
  def _updatePlot(self, sinogram):
134
- self._plot.getPlotWidget().addImage(data=sinogram)
135
- self._plot.getPlotWidget().replot()
131
+ self._plot.addImage(data=sinogram)
132
+ self._plot.replot()
136
133
 
137
134
  def _sinogram_loaded(self):
138
135
  """callback when the sinogram is loaded"""
139
- self._plot.setWaiting(False)
136
+ self._plotWaiter.hide()
140
137
  if self._scan is None or self._scan() is None:
141
138
  return
142
139
  assert self._sinoInfoCache is not None
@@ -161,7 +158,7 @@ class SinogramViewer(qt.QMainWindow):
161
158
  )
162
159
  return
163
160
  # update scan
164
- self._plot.setWaiting(True)
161
+ self._plotWaiter.show()
165
162
  self.sigSinoLoadStarted.emit()
166
163
  self._sinoInfoCache = (
167
164
  str(self._scan()),
@@ -176,18 +173,14 @@ class SinogramViewer(qt.QMainWindow):
176
173
  self._loadingThread.start()
177
174
 
178
175
  def clear(self):
179
- self._plot.getPlotWidget().clear()
176
+ self._plot.clear()
180
177
 
181
178
  def close(self):
182
- self._stopAnimationThread()
179
+ self._plotWaiter.hide()
183
180
  self._plot.close()
184
181
  self._plot = None
185
182
  super().close()
186
183
 
187
- def _stopAnimationThread(self):
188
- if self._plot is not None:
189
- self._plot.stopUpdateThread()
190
-
191
184
 
192
185
  class _LoadSinoThread(qt.QThread):
193
186
  def init(self, data, line, subsampling):
@@ -1,84 +1,82 @@
1
- # coding: utf-8
2
- # /*##########################################################################
3
- # Copyright (C) 2016 European Synchrotron Radiation Facility
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- # THE SOFTWARE.
22
- #
23
- #############################################################################*/
24
-
25
-
26
- __authors__ = ["H. Payno"]
27
- __license__ = "MIT"
28
- __date__ = "21/06/2021"
29
-
30
-
31
1
  import logging
32
2
  import os
33
- import shutil
34
- import tempfile
35
- import unittest
36
-
3
+ import pytest
37
4
  import numpy
5
+ from time import sleep
6
+
38
7
  from silx.gui import qt
39
- from silx.gui.utils.testutils import TestCaseQt
40
8
  from tomoscan.esrf.volume.hdf5volume import HDF5Volume
9
+ from tomoscan.esrf.volume.tiffvolume import TIFFVolume, has_tifffile
41
10
 
11
+ from tomwer.tests.conftest import qtapp # noqa F401
42
12
  from tomwer.core.utils.scanutils import MockNXtomo
43
13
  from tomwer.gui.visualization.volumeviewer import VolumeViewer
44
14
 
45
15
  logging.disable(logging.INFO)
46
16
 
47
17
 
48
- class TestDiffViewer(TestCaseQt):
49
- """unit test for the :class:_ImageStack widget"""
18
+ @pytest.mark.skipif(not has_tifffile, reason="tifffile not available")
19
+ def test_volume_viewer(
20
+ qtapp, # noqa F811
21
+ tmp_path,
22
+ ):
23
+ """
24
+ test the volume viewer setting a scan having an HDF5 volume linked to it
25
+ """
26
+
27
+ widget = VolumeViewer(parent=None)
28
+ widget.setAttribute(qt.Qt.WA_DeleteOnClose)
29
+
30
+ tmp_dir = tmp_path / "test_volume_viewer"
31
+ tmp_dir.mkdir()
32
+
33
+ # step 1 - test setting a scan containing a HDF5Volume
34
+ scan = MockNXtomo(
35
+ scan_path=os.path.join(str(tmp_dir), "myscan"),
36
+ n_proj=20,
37
+ n_ini_proj=20,
38
+ dim=10,
39
+ ).scan
40
+ volume = HDF5Volume(
41
+ file_path=os.path.join(scan.path, "volume.hdf5"),
42
+ data_path="entry",
43
+ data=numpy.random.random(60 * 10 * 10).reshape(60, 10, 10),
44
+ )
45
+ volume.save()
50
46
 
51
- def setUp(self):
52
- TestCaseQt.setUp(self)
53
- self._widget = VolumeViewer(parent=None)
54
- self._widget.setAttribute(qt.Qt.WA_DeleteOnClose)
47
+ scan.set_latest_vol_reconstructions(
48
+ [
49
+ volume,
50
+ ]
51
+ )
55
52
 
56
- self.tmp_dir = tempfile.mkdtemp()
53
+ widget.setScan(scan)
54
+ assert widget._centralWidget.data() is not None
55
+ widget.clear()
56
+ assert widget._centralWidget.data() is None
57
57
 
58
- self.scan = MockNXtomo(
59
- scan_path=os.path.join(self.tmp_dir, "myscan"),
60
- n_proj=20,
61
- n_ini_proj=20,
62
- dim=10,
63
- ).scan
64
- volume = HDF5Volume(
65
- file_path=os.path.join(self.scan.path, "volume.hdf5"),
66
- data_path="entry",
67
- data=numpy.random.random(60 * 10 * 10).reshape(60, 10, 10),
68
- )
69
- volume.save()
58
+ # step 2: test setting a a tiff volume dirrectly
59
+ volume = TIFFVolume(
60
+ folder=os.path.join(tmp_dir, "my_tiff_vol"),
61
+ data=numpy.random.random(60 * 100 * 100).reshape(60, 100, 100),
62
+ )
63
+ volume.save()
70
64
 
71
- self.scan.set_latest_vol_reconstructions(
72
- [
73
- volume,
74
- ]
75
- )
65
+ # 2.1 test with the data being in cache
66
+ widget.setVolume(volume=volume)
67
+ assert widget._centralWidget.data() is not None
68
+ widget.clear()
69
+ assert widget._centralWidget.data() is None
76
70
 
77
- def tearDown(self):
78
- shutil.rmtree(self.tmp_dir)
79
- self._widget.close()
80
- self._widget = None
81
- unittest.TestCase.tearDown(self)
71
+ # 2.2 test with the data not being in cache anymore
72
+ volume.clear_cache()
82
73
 
83
- def test(self):
84
- self._widget.setScan(self.scan)
74
+ widget.setVolume(volume=volume)
75
+ # wait_for_processing_finished
76
+ while qt.QApplication.instance().hasPendingEvents():
77
+ qt.QApplication.instance().processEvents()
78
+ sleep(1.2) # wait for the thread to be processed
79
+ while qt.QApplication.instance().hasPendingEvents():
80
+ qt.QApplication.instance().processEvents()
81
+ # end waiting
82
+ assert widget._centralWidget.data() is not None
@@ -1,35 +1,4 @@
1
- # coding: utf-8
2
- # /*##########################################################################
3
- #
4
- # Copyright (c) 2016-2017 European Synchrotron Radiation Facility
5
- #
6
- # Permission is hereby granted, free of charge, to any person obtaining a copy
7
- # of this software and associated documentation files (the "Software"), to deal
8
- # in the Software without restriction, including without limitation the rights
9
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- # copies of the Software, and to permit persons to whom the Software is
11
- # furnished to do so, subject to the following conditions:
12
- #
13
- # The above copyright notice and this permission notice shall be included in
14
- # all copies or substantial portions of the Software.
15
- #
16
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
- # THE SOFTWARE.
23
- #
24
- # ###########################################################################*/
25
-
26
- __authors__ = ["H. Payno"]
27
- __license__ = "MIT"
28
- __date__ = "05/08/2020"
29
-
30
-
31
1
  import logging
32
- import os
33
2
  import weakref
34
3
  import h5py
35
4
  from typing import Union
@@ -46,11 +15,11 @@ from tomoscan.esrf.volume.hdf5volume import HDF5Volume
46
15
  from tomoscan.factory import Factory
47
16
  from tomoscan.identifier import VolumeIdentifier
48
17
  from tomoscan.volumebase import VolumeBase
49
- from tomoscan.io import HDF5File
18
+ from tomoscan.io import HDF5File, get_swmr_mode
50
19
 
51
20
  from tomwer.core.scan.scanbase import TomwerScanBase
52
- from tomwer.core.utils.ftseriesutils import get_vol_file_shape
53
21
  from tomwer.gui.visualization.reconstructionparameters import ReconstructionParameters
22
+ from silx.gui.widgets.WaitingOverlay import WaitingOverlay
54
23
 
55
24
  _logger = logging.getLogger(__name__)
56
25
 
@@ -247,6 +216,9 @@ class _TomoApplicationContext(DataViewHooks):
247
216
  class VolumeViewer(qt.QMainWindow):
248
217
  def __init__(self, parent):
249
218
  qt.QMainWindow.__init__(self, parent)
219
+
220
+ self._volume_loaded_in_background = (None, None)
221
+ # store the volume loaded in the background and the thread used for it as (volume_identifier, thread)
250
222
  self._centralWidget = DataViewerFrame(parent=self)
251
223
  self.__context = _TomoApplicationContext(self)
252
224
  self._centralWidget.setGlobalHooks(self.__context)
@@ -254,6 +226,12 @@ class VolumeViewer(qt.QMainWindow):
254
226
  qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding
255
227
  )
256
228
  self.setCentralWidget(self._centralWidget)
229
+ # waiter overlay to notify user loading is on-going
230
+ self._waitingOverlay = WaitingOverlay(self._centralWidget)
231
+ self._waitingOverlay.setIconSize(qt.QSize(30, 30))
232
+ self._waitingOverlay.hide()
233
+
234
+ # display scan information when possible
257
235
  self._infoWidget = _ScanInfo(parent=self)
258
236
 
259
237
  # top level dock widget to display information regarding the scan
@@ -308,9 +286,12 @@ class VolumeViewer(qt.QMainWindow):
308
286
  if volume is None:
309
287
  return
310
288
  self._set_volumes(volumes=(volume,))
311
- # TODO in the future: do the equivalent of setScan from the volume metadata
312
289
 
313
290
  def _get_data_volume(self, volume: VolumeBase):
291
+ """
292
+ load the data of the requested volume.
293
+ :return: (data: Optional[str], state: str) state can be "loaded", "loading" or "failed"
294
+ """
314
295
  if not isinstance(volume, VolumeBase):
315
296
  raise TypeError(
316
297
  f"volume is expected to be an instance of {VolumeBase}. Not {type(volume)}"
@@ -318,18 +299,66 @@ class VolumeViewer(qt.QMainWindow):
318
299
 
319
300
  self._close_h5_file()
320
301
  if isinstance(volume, HDF5Volume):
321
- self._h5_file = HDF5File(filename=volume.data_url.file_path(), mode="r")
302
+ self._h5_file = HDF5File(
303
+ filename=volume.data_url.file_path(), mode="r", swmr=get_swmr_mode()
304
+ )
322
305
  if volume.data_url.data_path() in self._h5_file:
323
306
  data = self._h5_file[volume.data_url.data_path()]
307
+ state = "loaded"
324
308
  else:
325
309
  data = None
310
+ state = "failed"
311
+ elif volume.data is not None:
312
+ data = volume.data
313
+ state = "loaded"
326
314
  else:
327
- _logger.warning(
328
- "Attempt to set a non HDF5 volume to the viewer. This requires to load all the data in memory. This can take a while"
329
- )
330
- data = volume.data if volume.data is not None else volume.load_data()
315
+ volume_id, _ = self._volume_loaded_in_background
316
+ if volume_id == volume.get_identifier().to_str():
317
+ # special case if the user send several time the volume which is currently loading
318
+ # in this case we just want to ignore the request to avoid reloading the volume
319
+ # can happen in the case of a large volume that take some time to be loaded.
320
+ pass
321
+ else:
322
+ _logger.warning(
323
+ "Attempt to set a non HDF5 volume to the viewer. This requires to load all the data in memory. This can take a while"
324
+ )
325
+ self._stopExistingLoaderThread()
326
+ self._loadAndDisplay(volume)
327
+ state = "loading"
328
+ data = None
329
+
330
+ return data, state
331
+
332
+ def _loaderThreadFinished(self):
333
+ """Callback activated when a VolumeLoader thread is finished"""
334
+ sender = self.sender()
335
+ if not isinstance(sender, VolumeLoader):
336
+ raise TypeError("sender is expected to be a VolumeLoader")
331
337
 
332
- return data
338
+ if sender.volume.data is None:
339
+ _logger.error(f"Failed to load volume {sender.volume.get_identifier()}")
340
+
341
+ self._stopExistingLoaderThread()
342
+ self.setVolume(sender.volume)
343
+
344
+ def _loadAndDisplay(self, volume):
345
+ """Load a thread and add a callback when loading is done"""
346
+ loader_thread = VolumeLoader(volume=volume)
347
+ self._volume_loaded_in_background = (
348
+ volume.get_identifier().to_str(),
349
+ loader_thread,
350
+ )
351
+ loader_thread.finished.connect(self._loaderThreadFinished)
352
+ loader_thread.start()
353
+
354
+ def _stopExistingLoaderThread(self):
355
+ """Will stop any existing loader thread. Make sure we load one volume at most at the time"""
356
+ _, loader_thread = self._volume_loaded_in_background
357
+ if loader_thread is not None:
358
+ loader_thread.finished.disconnect(self._loaderThreadFinished)
359
+ if loader_thread.isRunning():
360
+ loader_thread.quit()
361
+ self._volume_loaded_in_background = (None, None)
333
362
 
334
363
  def _set_volumes(self, volumes: tuple):
335
364
  self.clear()
@@ -351,11 +380,7 @@ class VolumeViewer(qt.QMainWindow):
351
380
  f"Volume should be an instance of a Volume, a VolumeIdentifier or a string refering to a VolumeIdentifier. {type(volume)} provided"
352
381
  )
353
382
 
354
- data = self._get_data_volume(volume)
355
- # set volume dataset
356
- if data is not None:
357
- self._set_volume(data)
358
- # set reconstruction parameters
383
+ # warning: load metadata before data because can get some conflict with the HDF5 reader flag if done after
359
384
  try:
360
385
  # warning: limitation expected for .vol as it gets two configuration file. The default one is vol.info and does not contains
361
386
  # any of the metadata 'distance', 'pixel size'... but it is here for backward compatiblity
@@ -367,6 +392,21 @@ class VolumeViewer(qt.QMainWindow):
367
392
  f"Unable to set reconstruction parameters from {volume.data_url}. Not handled for pyhst reconstructions. Error is {e}"
368
393
  )
369
394
 
395
+ data, state = self._get_data_volume(volume)
396
+ # set volume dataset
397
+ if state == "loading":
398
+ self._waitingOverlay.show()
399
+ elif state == "loaded":
400
+ self._waitingOverlay.hide()
401
+ if data is not None:
402
+ self._set_volume(data)
403
+
404
+ elif state == "failed":
405
+ _logger.warning(
406
+ f"Failed to load data from {volume.get_identifier().to_str()}"
407
+ )
408
+ return
409
+
370
410
  def _set_volume(self, volume: Union[numpy.ndarray, h5py.Dataset]):
371
411
  self._centralWidget.setData(volume)
372
412
  if self.__first_load:
@@ -380,68 +420,28 @@ class VolumeViewer(qt.QMainWindow):
380
420
  self._close_h5_file()
381
421
  # self._centralWidget.setData(None)
382
422
  self._infoWidget.clear()
423
+ self._centralWidget.setData(None)
424
+ # if clear stop loading any volume
425
+ self._stopExistingLoaderThread()
383
426
 
384
427
  def sizeHint(self):
385
428
  return qt.QSize(600, 600)
386
429
 
387
- # TODO: should be merged with dataviewer._load_vol function ?
388
- def _load_vol(self, url):
389
- """
390
- load a .vol file
391
- """
392
- if url.file_path().lower().endswith(".vol.info"):
393
- info_file = url.file_path()
394
- raw_file = url.file_path().replace(".vol.info", ".vol")
395
- else:
396
- assert url.file_path().lower().endswith(".vol")
397
- raw_file = url.file_path()
398
- info_file = url.file_path().replace(".vol", ".vol.info")
399
430
 
400
- if not os.path.exists(raw_file):
401
- data = None
402
- mess = f"Can't find raw data file {raw_file} associated with {info_file}"
403
- _logger.warning(mess)
404
- elif not os.path.exists(info_file):
405
- mess = f"Can't find info file {info_file} associated with {raw_file}"
406
- _logger.warning(mess)
407
- data = None
408
- else:
409
- shape = get_vol_file_shape(info_file)
410
- if None in shape:
411
- _logger.warning(f"Fail to retrieve data shape for {info_file}.")
412
- data = None
413
- else:
414
- try:
415
- numpy.zeros(shape)
416
- except MemoryError:
417
- data = None
418
- _logger.warning(f"Raw file {raw_file} is too large for being read")
419
- else:
420
- data = numpy.fromfile(
421
- raw_file, dtype=numpy.float32, count=-1, sep=""
422
- )
423
- try:
424
- data = data.reshape(shape)
425
- except ValueError:
426
- _logger.warning(
427
- f"unable to fix shape for raw file {raw_file}. "
428
- "Look for information in {info_file}"
429
- )
430
- try:
431
- sqr = int(numpy.sqrt(len(data)))
432
- shape = (1, sqr, sqr)
433
- data = data.reshape(shape)
434
- except ValueError:
435
- _logger.info(
436
- f"deduction of shape size for {raw_file} " "failed"
437
- )
438
- data = None
439
- else:
440
- _logger.warning(
441
- f"try deducing shape size for {raw_file} "
442
- "might be an incorrect interpretation"
443
- )
444
- if url.data_slice() is None:
445
- return data
446
- else:
447
- return data[url.data_slice()]
431
+ class VolumeLoader(qt.QThread):
432
+ """
433
+ simple thread that load a volume in memory
434
+ """
435
+
436
+ def __init__(self, volume: VolumeBase) -> None:
437
+ super().__init__()
438
+ if not isinstance(volume, VolumeBase):
439
+ raise TypeError()
440
+ self.__volume = volume
441
+
442
+ def run(self):
443
+ self.__volume.load_data(store=True)
444
+
445
+ @property
446
+ def volume(self):
447
+ return self.__volume