├── .gitignore
├── .travis.yml
├── HISTORY.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── examples
├── 00_using_from_python.py
├── 01_using_from_python_without_class_reference.py
├── 02_processing_function.py
└── 03_annotate.py
├── pimsviewer
├── __init__.py
├── dimension.py
├── dimension.ui
├── example_plugins.py
├── gui.py
├── imagewidget.py
├── mainwindow.ui
├── pims_image.py
├── plugins.py
├── scroll_message_box.py
├── tests
│ ├── __init__.py
│ ├── test_gui.py
│ └── test_plugins.py
├── utils.py
└── wrapped_reader.py
├── screenshot.png
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[co]
2 |
3 | # Packages
4 | *.egg
5 | *.egg-info
6 | dist
7 | build
8 | eggs
9 | parts
10 | bin
11 | var
12 | sdist
13 | develop-eggs
14 | .installed.cfg
15 |
16 | # Installer logs
17 | pip-log.txt
18 |
19 | # Unit test / coverage reports
20 | .coverage
21 | .tox
22 |
23 | #Translations
24 | *.mo
25 |
26 | #Mr Developer
27 | .mr.developer.cfg
28 |
29 | *.pro
30 | *.log
31 | *.pyc
32 | *.so
33 | *~
34 | *.swp
35 |
36 | MANIFEST
37 |
38 | #PyCharm
39 | .idea
40 | tags
41 | .directory
42 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | sudo: false
3 |
4 | python:
5 | - "3.6"
6 | - "3.7"
7 |
8 | install:
9 | # needed to get xvfb running?
10 | - sudo apt-get install -y libegl1-mesa libdbus-1-3 libxkbcommon-x11-0
11 | - conda update --yes conda
12 | # Append the conda-forge channel, instead of adding it. See:
13 | # https://github.com/conda-forge/conda-forge.github.io/issues/232)
14 | - conda config --append channels conda-forge
15 | - conda create -n testenv --yes $DEPS nose pip python=$TRAVIS_PYTHON_VERSION
16 | - source activate testenv
17 | - python -m pip install .
18 |
19 | before_install:
20 | - wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh
21 | - chmod +x miniconda.sh
22 | - ./miniconda.sh -b -p /home/travis/mc
23 | - export PATH=/home/travis/mc/bin:$PATH
24 |
25 | script:
26 | # for running the GUI
27 | - xvfb-run --server-args="-screen 0 1024x768x24" nosetests --nologcapture
28 |
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | # Version 2.0
2 |
3 | * Pimsviewer is completely re-written from scratch, it is not backwards compatible
4 | * Performance is improved by using native PyQt canvases for drawing instead of Matplotlib
5 | * A file info dialog is added, displaying most information that PIMS can infer from the file
6 | * The plugin system is revamped and example plugins are added that are enabled by default, with the most notable one an Annotate plugin that can annotate positions saved to a CSV file from Trackpy
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Notice and Statement for the pimsviewer Project
2 | =========================================================
3 |
4 | Copyright (c) 2016-2017 pimsviewer contributors
5 | https://github.com/soft-matter/pimsviewer
6 | All rights reserved
7 |
8 | Redistribution and use in source and binary forms, with or without
9 | modification, are permitted provided that the following conditions are met:
10 | * Redistributions of source code must retain the above copyright
11 | notice, this list of conditions and the following disclaimer.
12 | * Redistributions in binary form must reproduce the above copyright
13 | notice, this list of conditions and the following disclaimer in the
14 | documentation and/or other materials provided with the distribution.
15 | * Neither the name of the soft-matter organization nor the
16 | names of its contributors may be used to endorse or promote products
17 | derived from this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
23 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include MANIFEST.in
2 | include setup.py
3 | include README.md
4 | include LICENSE
5 | include images/*
6 | recursive-include pimsviewer *.ui
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pimsviewer
2 | [](https://anaconda.org/conda-forge/pimsviewer)
3 |
4 | A graphical user interface (GUI) for PIMS (screenshot below)
5 |
6 | This viewer is based on [PyQt5](https://www.riverbankcomputing.com/software/pyqt/intro) and is able to work with N-dimensional image files that are opened by PIMS.
7 |
8 | Also, it has a plugin infrastructure that can be used to extend the main functionality.
9 |
10 | ## Installation
11 |
12 | Pimsviewer can be installed using conda:
13 |
14 | ```
15 | conda install -c conda-forge pimsviewer
16 | ```
17 |
18 | Alternatively, it can also be installed using pip:
19 |
20 | ```
21 | pip install pimsviewer
22 | ```
23 |
24 | When installing the latest source version, always install it with `pip` (and not with `python setup.py develop`, this will lead to dependency errors for `PyQt`):
25 |
26 | Normal installation:
27 |
28 | ```
29 | pip install .
30 | ```
31 |
32 | Development installation:
33 |
34 | ```
35 | pip install . -e
36 | ```
37 |
38 | ## Starting the viewer
39 |
40 | After installing the viewer, an executable `pimsviewer` is available. Simply run the command via your terminal/command line interface.
41 |
42 | ```
43 | $ pimsviewer --help
44 | Usage: pimsviewer [OPTIONS] [FILEPATH]
45 | Options:
46 | --example-plugins / --no-example-plugins
47 | Load additional example plugins
48 | --help Show this message and exit.
49 | ```
50 |
51 | ## Screenshot
52 |
53 | 
54 |
55 | ## Examples
56 |
57 | All examples below are also available as script files in the `examples` folder.
58 | By running `pimsviewer --example-plugins`, you can preview the example plugins used below.
59 |
60 | ## Example 00: Using the viewer from Python
61 |
62 | You can use the viewer in a Python script as follows:
63 |
64 | ```
65 | import sys
66 | from pimsviewer import GUI
67 | from PyQt5.QtWidgets import QApplication
68 |
69 | filepath = 'path/to/file'
70 |
71 | # Class names of extra plugins to add
72 | plugins = []
73 |
74 | app = QApplication(sys.argv)
75 | gui = GUI(extra_plugins=plugins)
76 | gui.open(fileName=filepath)
77 | gui.show()
78 |
79 | sys.exit(app.exec_())
80 | ```
81 |
82 | ## Example 01: Using the viewer from Python (the shorter way)
83 |
84 | Or, if you do not need a reference to the actual object but you just want to start the program:
85 |
86 | ```
87 | from pimsviewer import run
88 |
89 | run('path/to/file')
90 | ```
91 |
92 | In both cases, you can omit the file path.
93 |
94 | ## Example 02: evaluating the effect of a processing function
95 |
96 | This example adds a processing function that adds an adjustable amount of noise
97 | to an image. The amount of noise is tunable with a slider.
98 |
99 | ```
100 | from pimsviewer import run
101 | from pimsviewer.plugins import ProcessingPlugin
102 |
103 | run('path/to/file', [ProcessingPlugin])
104 | ```
105 |
106 | ## Example 03: annotating features on a video
107 |
108 | This example annotates features that were obtained via trackpy onto a video.
109 | Tracked positions are loaded from a pandas DataFrame CSV file by the user.
110 |
111 | ```
112 | from pimsviewer import run
113 | from pimsviewer.plugins import AnnotatePlugin
114 |
115 | run('path/to/file', [AnnotatePlugin])
116 | ```
117 |
118 | ## Your own plugin?
119 |
120 | By looking at the code for the example plugins, it should be fairly easy to
121 | extend pimsviewer using your own plugins. Contact one of the maintainers if you
122 | have any trouble writing your own plugins.
123 |
124 | # Authors
125 |
126 | Pimsviewer version 1.0 was written by [Casper van der Wel](https://github.com/caspervdw), versions starting from 2.0 are written by [Ruben Verweij](https://github.com/rbnvrw).
127 |
--------------------------------------------------------------------------------
/examples/00_using_from_python.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from pimsviewer import GUI
3 | from PyQt5.QtWidgets import QApplication
4 |
5 | filepath = 'path/to/file'
6 |
7 | # Class names of extra plugins to add
8 | plugins = []
9 |
10 | app = QApplication(sys.argv)
11 | gui = GUI(extra_plugins=plugins)
12 | gui.open(fileName=filepath)
13 | gui.show()
14 |
15 | sys.exit(app.exec_())
16 |
--------------------------------------------------------------------------------
/examples/01_using_from_python_without_class_reference.py:
--------------------------------------------------------------------------------
1 | from pimsviewer import run
2 |
3 | run('path/to/file')
4 |
--------------------------------------------------------------------------------
/examples/02_processing_function.py:
--------------------------------------------------------------------------------
1 | from pimsviewer import run
2 | from pimsviewer.example_plugins import ProcessingPlugin
3 |
4 | run('path/to/file', [ProcessingPlugin])
5 |
--------------------------------------------------------------------------------
/examples/03_annotate.py:
--------------------------------------------------------------------------------
1 | from pimsviewer import run
2 | from pimsviewer.example_plugins import AnnotatePlugin
3 |
4 | run('path/to/file', [AnnotatePlugin])
5 |
--------------------------------------------------------------------------------
/pimsviewer/__init__.py:
--------------------------------------------------------------------------------
1 | name = 'pimsviewer'
2 |
3 | from .gui import GUI, run
4 |
--------------------------------------------------------------------------------
/pimsviewer/dimension.py:
--------------------------------------------------------------------------------
1 | import os
2 | import numpy as np
3 | from PyQt5 import uic
4 | from PyQt5.QtCore import QDir, Qt, QTimer, pyqtSignal
5 | from PyQt5.QtGui import QImage, QPainter, QPalette, QPixmap
6 | from PyQt5.QtWidgets import (QHBoxLayout, QSlider, QWidget, QAction, QApplication, QFileDialog, QLabel, QMainWindow, QMenu, QMessageBox, QScrollArea, QSizePolicy, QStatusBar, QVBoxLayout, QDockWidget, QPushButton, QStyle, QLineEdit, QCheckBox, QInputDialog)
7 |
8 | class Dimension(QWidget):
9 |
10 | _playing = False
11 | _size = 0
12 | _position = 0
13 | _mergeable = False
14 | _merge = False
15 | _playable = False
16 | _fps = 5.0
17 | _max_playback_fps = 5.0
18 |
19 | play_event = pyqtSignal(QWidget)
20 |
21 | def __init__(self, name, size=0):
22 | super(Dimension, self).__init__()
23 |
24 | self.name = name
25 | self._size = size
26 |
27 | dirname = os.path.dirname(os.path.realpath(__file__))
28 | uic.loadUi(os.path.join(dirname, 'dimension.ui'), self)
29 |
30 | self.playButton.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
31 | self.playButton.clicked.connect(self.click_event)
32 |
33 | self.playTimer = QTimer()
34 | self.playTimer.timeout.connect(self.play_tick)
35 |
36 | self.posButton.pressed.connect(self.update_position_from_btn)
37 |
38 | self.slider.setMaximum(self.size-1)
39 | self.slider.valueChanged.connect(self.update_position_from_slider)
40 |
41 | self.mergeButton.clicked.connect(self.update_merge)
42 |
43 | if not self.mergeable:
44 | self.mergeButton.hide()
45 |
46 | self._merge = self.mergeButton.isChecked()
47 |
48 | self.fps = self._fps
49 | self.fpsButton.pressed.connect(self.fps_changed)
50 |
51 | self.hide()
52 |
53 | def merge_image_over_dimension(self, image):
54 | # problem here: could be two axes with same size
55 | # TODO: think of a clever fix for this
56 | try:
57 | ix = image.shape.index(self._size)
58 | except ValueError:
59 | return image
60 |
61 | if self.name != 'c':
62 | # I don't know what to do, sum over axis
63 | image = np.sum(image, axis=ix)
64 |
65 | return image
66 |
67 | def enable(self):
68 | if not self.playable:
69 | return
70 |
71 | self.setEnabled(True)
72 | self.playButton.setEnabled(True)
73 | self.posButton.setEnabled(True)
74 | self.slider.setEnabled(True)
75 | self.fpsButton.setEnabled(True)
76 |
77 | if self.mergeable:
78 | self.mergeButton.setEnabled(True)
79 | self.mergeButton.show()
80 |
81 | self.show()
82 |
83 | def disable(self):
84 | self.setEnabled(False)
85 | self.playButton.setEnabled(False)
86 | self.posButton.setEnabled(False)
87 | self.slider.setEnabled(False)
88 | self.fpsButton.setEnabled(False)
89 | self.mergeButton.setEnabled(False)
90 |
91 | def fps_changed(self):
92 | fps, ok = QInputDialog.getDouble(self, "Playback framerate", "New playback framerate", self.fps)
93 |
94 | if ok:
95 | self.fps = fps
96 |
97 | def click_event(self):
98 | if not self.playable:
99 | return
100 |
101 | if not self.playing:
102 | self.playing = True
103 | else:
104 | self.playing = False
105 |
106 | def play_tick(self):
107 | if not self.playing:
108 | return
109 |
110 | if self._fps > self._max_playback_fps:
111 | self.position += int(round(self._fps / self._max_playback_fps))
112 | else:
113 | self.position += 1
114 |
115 | @property
116 | def size(self):
117 | return self._size
118 |
119 | @size.setter
120 | def size(self, size):
121 | self._size = size
122 | self.position = 0
123 | self.playing = False
124 | self.slider.setMinimum(0)
125 | self.slider.setMaximum(self.size-1)
126 |
127 | @property
128 | def fps(self):
129 | return self._fps
130 |
131 | @fps.setter
132 | def fps(self, fps):
133 | fps = float(fps)
134 |
135 | self._fps = fps
136 | play_fps = fps if fps < self._max_playback_fps else self._max_playback_fps
137 | self.playTimer.setInterval(int(round(1000.0 / play_fps)))
138 | self.fpsButton.setText('%d fps' % self.fps)
139 |
140 | @property
141 | def playable(self):
142 | return self._playable
143 |
144 | @playable.setter
145 | def playable(self, playable):
146 | self._playable = bool(playable)
147 |
148 | @property
149 | def playing(self):
150 | return self._playing
151 |
152 | @playing.setter
153 | def playing(self, playing):
154 | self._playing = bool(playing)
155 | if self._playing:
156 | self.playTimer.start()
157 | else:
158 | self.playTimer.stop()
159 |
160 | @property
161 | def position(self):
162 | return self._position
163 |
164 | def update_position_from_slider(self):
165 | position = self.slider.value()
166 | if position >= 0:
167 | self.position = position
168 |
169 | def update_position_from_btn(self):
170 | position, ok = QInputDialog.getInt(self, "'%s' position" % self.name, "New '%s' position (0-%d)" % (self.name, self.size-1), self.position, 0, self.size-1)
171 |
172 | if ok:
173 | self.position = position
174 |
175 | @position.setter
176 | def position(self, position):
177 | old_position = self.position
178 |
179 | while position < 0:
180 | position += self.size
181 |
182 | if position < self.size:
183 | self._position = position
184 | else:
185 | self._position = position - self.size
186 |
187 | self.slider.setValue(self.position)
188 | self.posButton.setText('%s=%d' % (self.name, self.position))
189 |
190 | if old_position != self.position:
191 | self.play_event.emit(self)
192 |
193 | def update_merge(self):
194 | self.merge = self.mergeButton.isChecked()
195 |
196 | @property
197 | def merge(self):
198 | return self._merge
199 |
200 | @merge.setter
201 | def merge(self, merge):
202 | if not self.mergeable:
203 | merge = False
204 |
205 | if merge != self._merge:
206 | self._merge = bool(merge)
207 | self.mergeButton.setChecked(self._merge)
208 | self.play_event.emit(self)
209 |
210 | @property
211 | def mergeable(self):
212 | return self._mergeable
213 |
214 | @mergeable.setter
215 | def mergeable(self, mergeable):
216 | self._mergeable = bool(mergeable)
217 | if not mergeable:
218 | self.merge = False
219 |
220 | def __len__(self):
221 | return self.size
222 |
223 | def __str__(self):
224 | classname = self.__class__.__name__
225 | playing = "playing" if self.playing else "not playing"
226 | return "<%s %s of length %d (%s)>" % (classname, self.name, self.size, playing)
227 |
228 | def __repr__(self):
229 | return self.__str__()
230 |
231 |
--------------------------------------------------------------------------------
/pimsviewer/dimension.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Dimension
4 |
5 |
6 | false
7 |
8 |
9 |
10 | 1
11 | 1
12 |
13 |
14 |
15 | -
16 |
17 |
18 | false
19 |
20 |
21 |
22 |
23 |
24 | true
25 |
26 |
27 |
28 | -
29 |
30 |
31 | false
32 |
33 |
34 | t = 0
35 |
36 |
37 |
38 | -
39 |
40 |
41 | false
42 |
43 |
44 | 50
45 |
46 |
47 | 0
48 |
49 |
50 | false
51 |
52 |
53 | Qt::Horizontal
54 |
55 |
56 | QSlider::TicksBelow
57 |
58 |
59 | 50
60 |
61 |
62 |
63 | -
64 |
65 |
66 | false
67 |
68 |
69 | Merge
70 |
71 |
72 | true
73 |
74 |
75 | true
76 |
77 |
78 |
79 | -
80 |
81 |
82 | 10 fps
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/pimsviewer/example_plugins.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from collections import deque
3 | from pims.display import to_rgb
4 | from os import path
5 |
6 | from PIL import Image, ImageQt
7 | from PyQt5.QtCore import QDir, Qt, QRectF
8 | from PyQt5.QtGui import QImage, QPainter, QPalette, QPixmap
9 | from PyQt5.QtWidgets import (QHBoxLayout, QSlider, QWidget, QAction, QApplication, QFileDialog, QLabel, QMainWindow, QMenu, QMessageBox, QScrollArea, QSizePolicy, QStatusBar, QVBoxLayout, QDockWidget, QPushButton, QStyle, QLineEdit, QDialog, QGraphicsEllipseItem, QCheckBox, QDoubleSpinBox)
10 |
11 | import pandas as pd
12 |
13 | from pimsviewer.utils import pixmap_from_array
14 | from pimsviewer.plugins import Plugin
15 |
16 | class AnnotatePlugin(Plugin):
17 | name = 'Annotate plugin'
18 |
19 | def __init__(self, parent=None, positions_df=None):
20 | super(AnnotatePlugin, self).__init__(parent)
21 |
22 | self.x_name = 'x'
23 | self.y_name = 'y'
24 | self.r_name = 'r'
25 |
26 | self.unit_scaling = None
27 | self.positions_df = positions_df
28 |
29 | self.vbox = QVBoxLayout()
30 | self.setLayout(self.vbox)
31 |
32 | self.label = QLabel('Annotate Plugin')
33 | self.description = QLabel('Loads trajectories from CSV files containing the columns frame,x,y(,r) and draws circles at the specified locations.')
34 | self.description.setWordWrap(True)
35 | self.vbox.addWidget(self.label)
36 | self.vbox.addWidget(self.description)
37 |
38 | self.browseBtn = QPushButton('Open trajectories...')
39 | self.browseBtn.clicked.connect(self.open)
40 | self.vbox.addWidget(self.browseBtn)
41 |
42 | self.scaleLabel = QLabel('Scale factor (units/px)')
43 | self.scaleInput = QDoubleSpinBox()
44 | self.scaleInput.setMinimum(0)
45 | self.scaleInput.setValue(1.0)
46 | self.scaleInput.setDecimals(5)
47 | self.scaleInput.setSingleStep(0.1)
48 | self.scaleInput.valueChanged.connect(self.set_unit_scaling)
49 | self.vbox.addWidget(self.scaleLabel)
50 | self.vbox.addWidget(self.scaleInput)
51 |
52 | self.swapXYSwitch = QCheckBox('Swap X and Y columns')
53 | self.swapXYSwitch.stateChanged.connect(self.swap_xy)
54 | self.swapXYSwitch.setChecked(False)
55 | self.vbox.addWidget(self.swapXYSwitch)
56 |
57 | self.items = []
58 |
59 | def clearAll(self, image_widget):
60 | for item in self.items:
61 | image_widget.removeItemFromScene(item)
62 |
63 | self.items = []
64 |
65 | def rect_from_xyr(self, x, y, r, scaleFactor):
66 | x_top_left = (x - r)*scaleFactor
67 | y_top_left = (y - r)*scaleFactor
68 | size = 2.0*r*scaleFactor
69 | return QRectF(x_top_left, y_top_left, size, size)
70 |
71 | def showFrame(self, image_widget, dimensions):
72 | if self.positions_df is None:
73 | return
74 | self.set_unit_scaling()
75 |
76 | scaleFactor = image_widget.scaleFactor
77 | self.clearAll(image_widget)
78 | frame_no = dimensions['t'].position
79 |
80 | selection = self.positions_df[self.positions_df['frame'] == frame_no]
81 |
82 | for i, row in selection.iterrows():
83 | x = float(row[self.x_name]) * self.unit_scaling
84 | y = float(row[self.y_name]) * self.unit_scaling
85 |
86 | r = float(row[self.r_name]) * self.unit_scaling
87 | if np.isnan(r):
88 | r = 10.0
89 |
90 | ellipse = QGraphicsEllipseItem(self.rect_from_xyr(x, y, r, scaleFactor))
91 | pen = ellipse.pen()
92 | pen.setWidth(2)
93 | pen.setColor(Qt.red)
94 | ellipse.setPen(pen)
95 | image_widget.addItemToScene(ellipse)
96 | self.items.append(ellipse)
97 |
98 | def swap_xy(self):
99 | if not self.swapXYSwitch.isChecked():
100 | self.x_name = 'x'
101 | self.y_name = 'y'
102 | else:
103 | self.x_name = 'y'
104 | self.y_name = 'x'
105 |
106 | def set_unit_scaling(self):
107 | if self.app.reader and self.unit_scaling is None:
108 | try:
109 | self.unit_scaling = 1.0 / self.app.reader.metadata['pixel_microns']
110 | except:
111 | self.unit_scaling = 1.0
112 | print('Warning: AnnotatePlugin: file type not implemented for automatic coordinate scaling')
113 |
114 | self.unit_scaling = self.scaleInput.value()
115 |
116 | def open(self):
117 | currentDir = QDir.currentPath()
118 | if self.app is not None:
119 | parentFile = self.app.filename
120 | currentDir = path.dirname(parentFile)
121 |
122 | fileName, _ = QFileDialog.getOpenFileName(self, "Open trajectories", currentDir)
123 | if fileName:
124 | try:
125 | self.positions_df = pd.read_csv(fileName)
126 | except Exception as exception:
127 | QMessageBox.critical(self, "Error", "Cannot load %s: %s" % (fileName, exception))
128 | return
129 |
130 | if self.app is not None:
131 | self.app.refreshPlugins()
132 |
133 | class ProcessingPlugin(Plugin):
134 | name = 'Processing plugin (example)'
135 | noise_level = 50
136 |
137 | def __init__(self, parent=None):
138 | super(ProcessingPlugin, self).__init__(parent)
139 |
140 | self.vbox = QVBoxLayout()
141 | self.setLayout(self.vbox)
142 |
143 | self.vbox.addWidget(QLabel('Example processing plugin'))
144 | self.vbox.addWidget(QLabel('Add noise:'))
145 |
146 | self.slider = QSlider(Qt.Horizontal)
147 | self.slider.setMinimum(10)
148 | self.slider.setMaximum(100)
149 | self.slider.setValue(50)
150 | self.slider.setTickPosition(QSlider.TicksBelow)
151 | self.slider.setTickInterval(10)
152 | self.slider.valueChanged.connect(self.update_noise)
153 | self.vbox.addWidget(self.slider)
154 |
155 | def update_noise(self):
156 | self.noise_level = self.slider.value()
157 | self.showFrame(self.parent().imageView, self.parent().dimensions)
158 |
159 | def activate(self):
160 | super(ProcessingPlugin, self).activate()
161 |
162 | self.showFrame(self.parent().imageView, self.parent().dimensions)
163 |
164 | def showFrame(self, image_widget, dimensions):
165 | arr = self.parent().get_current_frame()
166 |
167 | arr = arr + np.random.random(arr.shape) * self.noise_level / 100 * arr.max()
168 | arr = arr / np.max(arr)
169 |
170 | arr = (arr * 255.0).astype(np.uint8)
171 |
172 | image = pixmap_from_array(arr)
173 |
174 | image_widget.setPixmap(image)
175 |
176 |
--------------------------------------------------------------------------------
/pimsviewer/gui.py:
--------------------------------------------------------------------------------
1 | from os import path
2 | import sys
3 | import click
4 | from PyQt5 import uic
5 | from PyQt5.QtCore import QDir, Qt, QMimeData
6 | from PyQt5.QtGui import QImage, QPainter, QPalette, QPixmap, QImageWriter
7 | from PyQt5.QtWidgets import (QHBoxLayout, QSlider, QWidget, QAction, QApplication, QFileDialog, QLabel, QMainWindow, QMenu, QMessageBox, QScrollArea, QSizePolicy, QStatusBar, QVBoxLayout, QDockWidget, QPushButton, QStyle, QLineEdit)
8 |
9 | from pimsviewer.example_plugins import AnnotatePlugin, Plugin, ProcessingPlugin
10 | from pimsviewer.imagewidget import ImageWidget
11 | from pimsviewer.dimension import Dimension
12 | from pimsviewer.wrapped_reader import WrappedReader
13 | from pimsviewer.scroll_message_box import ScrollMessageBox
14 | from pimsviewer.utils import get_supported_extensions, get_all_files_in_dir
15 | import pims
16 | import numpy as np
17 |
18 | try:
19 | from nd2reader import ND2Reader # remove after pims update
20 | except:
21 | pass
22 |
23 | class GUI(QMainWindow):
24 | name = "Pimsviewer"
25 |
26 | def __init__(self, extra_plugins=[]):
27 | super(GUI, self).__init__()
28 |
29 | dirname = path.dirname(path.realpath(__file__))
30 | uic.loadUi(path.join(dirname, 'mainwindow.ui'), self)
31 |
32 | self.setWindowTitle(self.name)
33 | self.setCentralWidget(self.imageView)
34 | self.setStatusBar(self.statusbar)
35 |
36 | self.imageView.hover_event.connect(self.image_hover_event)
37 | self.reader = None
38 | self.dimensions = {}
39 | self.filename = None
40 |
41 | self.init_dimensions()
42 |
43 | self.plugins = []
44 | self.pluginActions = []
45 | self.init_plugins(extra_plugins)
46 |
47 | def init_plugins(self, extra_plugins=[]):
48 | self.plugins = []
49 |
50 | for plugin_name in extra_plugins:
51 | extra_plugin = plugin_name(parent=self)
52 | if isinstance(extra_plugin, Plugin):
53 | self.plugins.append(extra_plugin)
54 |
55 | for plugin in self.plugins:
56 | action = QAction(plugin.name, self, triggered=plugin.activate)
57 | self.pluginActions.append(action)
58 | self.menuPlugins.addAction(action)
59 |
60 | def add_to_dock(self, widget):
61 | self.dockLayout.addWidget(widget)
62 |
63 | def updateWindowTitle(self):
64 | title = self.name
65 | if self.filename is not None:
66 | title += ' - %s' % path.basename(self.filename)
67 | self.setWindowTitle(title)
68 |
69 | def updateActions(self):
70 | hasfile = self.reader is not None
71 | self.actionClose.setEnabled(hasfile)
72 | self.actionFile_information.setEnabled(hasfile)
73 | self.actionSave.setEnabled(hasfile)
74 | self.actionOpen_next.setEnabled(hasfile)
75 | self.actionOpen_previous.setEnabled(hasfile)
76 | self.actionCopy.setEnabled(hasfile)
77 |
78 | fitWidth = self.actionFit_width.isChecked()
79 | self.actionZoom_in.setEnabled(not fitWidth)
80 | self.actionZoom_out.setEnabled(not fitWidth)
81 | self.actionNormal_size.setEnabled(not fitWidth)
82 |
83 | if not fitWidth:
84 | self.actionZoom_in.setEnabled(True)
85 | self.actionZoom_out.setEnabled(True)
86 |
87 | def zoomIn(self):
88 | self.imageView.scaleImage(1.25)
89 | self.refreshPlugins()
90 |
91 | def zoomOut(self):
92 | self.imageView.scaleImage(0.8)
93 | self.refreshPlugins()
94 |
95 | def normalSize(self):
96 | self.imageView.scaleImage(1.0, absolute=True)
97 | self.refreshPlugins()
98 |
99 | def fitToWindow(self):
100 | fitToWindow = self.actionFit_width.isChecked()
101 | self.imageView.fitWindow = fitToWindow
102 |
103 | self.imageView.doResize()
104 | self.updateActions()
105 |
106 | self.refreshPlugins()
107 |
108 | def about(self):
109 | QMessageBox.about(self, "About Pimsviewer",
110 | "Viewer for Python IMage Sequence (PIMS).
" +
111 | "See the Pimsviewer Github page for more information.
")
112 |
113 | def export(self):
114 | allowed_formats = QImageWriter.supportedImageFormats()
115 | allowed_formats = [str(f.data(), encoding='utf-8') for f in allowed_formats]
116 | filter_string = 'Images ('
117 | for f in allowed_formats:
118 | filter_string += '*.%s ' % f
119 | filter_string = filter_string[:-1] + ')'
120 |
121 | fileName, _ = QFileDialog.getSaveFileName(self, "Export File", QDir.currentPath(), filter_string)
122 | if not fileName:
123 | return
124 |
125 | self.imageView.image.pixmap().save(fileName)
126 | self.statusbar.showMessage('Image exported to %s' % fileName)
127 |
128 | def show_file_info(self):
129 | items = []
130 |
131 | # Metadata
132 | for prop in self.reader.metadata:
133 | html = '%s:
%s
' % (prop, self.reader.metadata[prop])
134 | items.append(html)
135 |
136 | try:
137 | html = 'Framerate:
%.3f
' % (self.reader.frame_rate)
138 | items.append(html)
139 | num_frames = self.reader.sizes['t']
140 | html = 'Duration:
%.3f s
' % (num_frames / self.reader.frame_rate)
141 | items.append(html)
142 | except (AttributeError):
143 | html = 'Framerate:
Unknown
'
144 | items.append(html)
145 | num_frames = self.reader.sizes['t']
146 | html = 'Duration:
%d frames
' % (num_frames)
147 | items.append(html)
148 |
149 | # File path
150 | html = 'Filename:
%s
' % (self.filename)
151 | items.append(html)
152 |
153 | # Reader
154 | if isinstance(self.reader, WrappedReader):
155 | reader_type = type(self.reader.reader).__name__
156 | else:
157 | reader_type = type(self.reader).__name__
158 | html = 'PIMS reader:
%s
%s
' % (reader_type, self.reader.__repr__())
159 | items.append(html)
160 |
161 | ScrollMessageBox(items, parent=self)
162 |
163 | def open(self, checked=False, fileName=None):
164 | if self.reader is not None:
165 | self.close_file()
166 |
167 | if fileName is None:
168 | fileName, _ = QFileDialog.getOpenFileName(self, "Open File", QDir.currentPath())
169 |
170 | if fileName:
171 | try:
172 | self.reader = WrappedReader(pims.open(fileName))
173 | except:
174 | QMessageBox.critical(self, "Error", "Cannot load %s." % fileName)
175 | return
176 |
177 | self.filename = fileName
178 | self.update_dimensions()
179 | self.showFrame()
180 |
181 | self.actionFit_width.setEnabled(True)
182 | self.updateActions()
183 | self.updateWindowTitle()
184 |
185 | def open_next_prev(self):
186 | direction_next = True
187 | if self.sender().objectName() == "actionOpen_previous":
188 | direction_next = False
189 |
190 | supported_extensions = get_supported_extensions()
191 |
192 | current_directory = path.dirname(self.filename)
193 | file_list = get_all_files_in_dir(current_directory, extensions=supported_extensions)
194 | if len(file_list) < 2:
195 | self.statusbar.showMessage('No file found for opening')
196 | return
197 |
198 | try:
199 | current_file_index = file_list.index(path.basename(self.filename))
200 | except ValueError:
201 | self.statusbar.showMessage('No file found for opening')
202 | return
203 |
204 | next_index = current_file_index + 1 if direction_next else current_file_index - 1
205 | try:
206 | next_file = file_list[next_index]
207 | except IndexError:
208 | next_index = 0 if direction_next else -1
209 | next_file = file_list[next_index]
210 |
211 | self.open(fileName=path.join(current_directory, next_file))
212 |
213 | def copy_image_to_clipboard(self):
214 | data = QMimeData()
215 | data.setImageData(self.imageView.image.pixmap())
216 | app = QApplication.instance()
217 | app.clipboard().setMimeData(data)
218 |
219 | def close_file(self):
220 | self.reader.close()
221 | self.reader = None
222 | self.filename = None
223 | self.showFrame()
224 | self.updateWindowTitle()
225 |
226 | def init_dimensions(self):
227 | for dim in 'tvzcxy':
228 | self.dimensions[dim] = Dimension(dim, 0)
229 | self.dimensions[dim].play_event.connect(self.play_event)
230 | if dim not in ['x', 'y']:
231 | self.add_to_dock(self.dimensions[dim])
232 | self.dimensions[dim].playable = True
233 | if dim in ['c', 'z', 'v']:
234 | self.dimensions[dim].mergeable = True
235 | else:
236 | self.dimensions[dim].mergeable = False
237 | self.dimensions[dim].merge = False
238 |
239 | def play_event(self, dimension):
240 | if not self.reader:
241 | return
242 |
243 | if len(self.reader.iter_axes) > 0:
244 | current_player = self.reader.iter_axes[0]
245 | if dimension.name != current_player:
246 | self.dimensions[current_player].playing = False
247 |
248 | self.reader.iter_axes = dimension.name
249 | self.showFrame()
250 |
251 | def image_hover_event(self, point):
252 | self.statusbar.showMessage('[%.1f, %.1f]' % (point.x(), point.y()))
253 |
254 | def update_dimensions(self):
255 | sizes = self.reader.sizes
256 |
257 | for dim in self.dimensions:
258 | if dim in sizes and sizes[dim] > 1:
259 | self.dimensions[dim].size = sizes[dim]
260 | self.dimensions[dim].enable()
261 | else:
262 | if dim in sizes:
263 | self.dimensions[dim].size = sizes[dim]
264 | else:
265 | self.dimensions[dim].size = 0
266 |
267 | self.dimensions[dim].disable()
268 | self.dimensions[dim].hide()
269 |
270 | # current playing axis
271 | self.reader.iter_axes = ''
272 |
273 | bundle_axes = ''
274 | if 'y' in self.reader.sizes:
275 | bundle_axes += 'y'
276 | if 'x' in self.reader.sizes:
277 | bundle_axes += 'x'
278 |
279 | self.reader.bundle_axes = bundle_axes
280 |
281 | if 't' in self.dimensions:
282 | try:
283 | self.dimensions['t'].fps = self.reader.frame_rate
284 | except AttributeError:
285 | self.statusbar.showMessage('Unable to read frame rate from file')
286 |
287 | def get_current_frame(self):
288 | self.reader.bundle_axes = 'xy'
289 | default_coords = {}
290 |
291 | for dim in self.dimensions:
292 | if dim not in self.reader.sizes:
293 | continue
294 |
295 | dim_obj = self.dimensions[dim]
296 |
297 | default_coords[dim] = dim_obj.position
298 |
299 | if dim_obj.merge and dim not in self.reader.bundle_axes:
300 | self.reader.bundle_axes += dim
301 |
302 | self.reader.default_coords = default_coords
303 |
304 | # always one playing axis at a time
305 | if len(self.reader.iter_axes) > 1:
306 | self.reader.iter_axes = self.reader.iter_axes[0]
307 |
308 | if len(self.reader.iter_axes) > 0:
309 | try:
310 | dim_obj = self.dimensions[self.reader.iter_axes[0]]
311 | i = dim_obj.position
312 | except KeyError:
313 | i = 0
314 | else:
315 | i = 0
316 |
317 | try:
318 | frame = self.reader[i]
319 | except IndexError:
320 | self.statusbar.showMessage('Unable to find %s=%d' % (dim_obj.name, i))
321 | frame = self.reader[0]
322 |
323 | if 'c' in self.reader.bundle_axes:
324 | cix = self.reader.bundle_axes.index('c')
325 | if cix != 0:
326 | frame = np.swapaxes(frame, 0, cix)
327 | frame = np.swapaxes(frame, 1, 2).copy()
328 |
329 | return frame
330 |
331 | def refreshPlugins(self):
332 | for plugin in self.plugins:
333 | if plugin.active:
334 | plugin.showFrame(self.imageView, self.dimensions)
335 |
336 | def showFrame(self):
337 | if self.reader is None:
338 | self.imageView.setPixmap(None)
339 | return
340 |
341 | if len(self.dimensions) == 0:
342 | self.update_dimensions()
343 |
344 | image_data = self.get_current_frame()
345 | for bdim in self.reader.bundle_axes:
346 | if bdim in ['x', 'y']:
347 | continue
348 | image_data = self.dimensions[bdim].merge_image_over_dimension(image_data)
349 |
350 | self.imageView.setPixmap(image_data)
351 | self.refreshPlugins()
352 |
353 | @click.command()
354 | @click.argument('filepath', required=False, type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, resolve_path=True))
355 | @click.option('--example-plugins/--no-example-plugins', default=True, help='Load additional example plugins')
356 | def run(filepath, example_plugins):
357 | app = QApplication(sys.argv)
358 |
359 | if example_plugins:
360 | extra_plugins = [AnnotatePlugin, ProcessingPlugin]
361 | else:
362 | extra_plugins = []
363 |
364 | gui = GUI(extra_plugins=extra_plugins)
365 | if filepath is not None:
366 | gui.open(fileName=filepath)
367 | gui.show()
368 |
369 | sys.exit(app.exec_())
370 |
371 | if __name__ == '__main__':
372 | run()
373 |
--------------------------------------------------------------------------------
/pimsviewer/imagewidget.py:
--------------------------------------------------------------------------------
1 | import pims
2 | import numpy as np
3 | from PyQt5.QtCore import QDir, Qt, QSize, QRectF, pyqtSignal, QPointF
4 | from PyQt5.QtGui import QImage, QPainter, QPalette, QPixmap
5 | from PyQt5.QtWidgets import (QAction, QApplication, QFileDialog, QLabel, QMainWindow, QMenu, QMessageBox, QScrollArea, QSizePolicy, QGraphicsView, QGraphicsScene)
6 |
7 | from pimsviewer.pims_image import PimsImage
8 | from pimsviewer.utils import image_to_pixmap
9 |
10 | class ImageWidget(QGraphicsView):
11 |
12 | hover_event = pyqtSignal(QPointF)
13 |
14 | def __init__(self, parent=None):
15 | super(ImageWidget, self).__init__(parent)
16 |
17 | self.scene = QGraphicsScene(self)
18 | self.scene.setSceneRect(QRectF())
19 | self.setScene(self.scene)
20 |
21 | self.image = PimsImage(self)
22 | self.scene.addItem(self.image)
23 |
24 | self.setDragMode(QGraphicsView.ScrollHandDrag)
25 |
26 | self.fitWindow = True
27 |
28 | self.doResize()
29 |
30 | def addItemToScene(self, item):
31 | self.scene.addItem(item)
32 |
33 | def removeItemFromScene(self, item):
34 | self.scene.removeItem(item)
35 |
36 | def setPixmap(self, pixmap):
37 | if pixmap is None:
38 | self.image.setVisible(False)
39 | return
40 |
41 | if isinstance(pixmap, QImage):
42 | pixmap = image_to_pixmap(image)
43 |
44 | if not self.image.isVisible():
45 | self.image.setVisible(True)
46 |
47 | if not isinstance(pixmap, QPixmap):
48 | pixmap = self.image.array_to_pixmap(pixmap)
49 |
50 | self.image.setPixmap(pixmap)
51 | self.doResize()
52 |
53 | def resizeEvent(self, event):
54 | super(ImageWidget, self).resizeEvent(event)
55 | self.doResize()
56 |
57 | def doResize(self):
58 | self.scaleImage(1.0)
59 |
60 | def adjustSize(self):
61 | super(ImageWidget, self).adjustSize()
62 | self.doResize()
63 |
64 | def scaleImage(self, factor, absolute=False):
65 | if not absolute:
66 | factor = self.image.scale() * factor
67 |
68 | if not self.fitWindow:
69 | self.image.setScale(factor)
70 | else:
71 | self.fitInView(self.image, Qt.KeepAspectRatio)
72 |
73 | @property
74 | def scaleFactor(self):
75 | return self.image.scale()
76 |
77 |
78 |
--------------------------------------------------------------------------------
/pimsviewer/mainwindow.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | MainWindow
4 |
5 |
6 |
7 | 0
8 | 0
9 | 706
10 | 592
11 |
12 |
13 |
14 | Pimsviewer
15 |
16 |
17 |
18 |
19 | 1
20 | 1
21 |
22 |
23 |
24 |
25 | 100
26 | 100
27 |
28 |
29 |
30 |
87 |
88 |
89 |
90 |
91 | 1
92 | 1
93 |
94 |
95 |
96 | QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable
97 |
98 |
99 | 8
100 |
101 |
102 |
103 |
104 | 1
105 | 1
106 |
107 |
108 |
109 |
110 | 0
111 |
112 |
113 | 0
114 |
115 |
116 | 0
117 |
118 |
119 | 0
120 |
121 |
122 | 0
123 |
124 |
125 |
126 |
127 |
128 |
129 | Open
130 |
131 |
132 | Ctrl+O
133 |
134 |
135 |
136 |
137 | false
138 |
139 |
140 | Open next
141 |
142 |
143 | Alt+Shift+O
144 |
145 |
146 |
147 |
148 | false
149 |
150 |
151 | Open previous
152 |
153 |
154 | Ctrl+Shift+O
155 |
156 |
157 |
158 |
159 | Open with
160 |
161 |
162 | Alt+O
163 |
164 |
165 |
166 |
167 | Info
168 |
169 |
170 |
171 |
172 | false
173 |
174 |
175 | Close
176 |
177 |
178 | Ctrl+W
179 |
180 |
181 |
182 |
183 | false
184 |
185 |
186 | Zoom in
187 |
188 |
189 | +
190 |
191 |
192 |
193 |
194 | false
195 |
196 |
197 | Zoom out
198 |
199 |
200 | -
201 |
202 |
203 |
204 |
205 | false
206 |
207 |
208 | Normal size
209 |
210 |
211 | 0
212 |
213 |
214 |
215 |
216 | true
217 |
218 |
219 | true
220 |
221 |
222 | Fit width
223 |
224 |
225 | Ctrl+F
226 |
227 |
228 |
229 |
230 | About
231 |
232 |
233 | F1
234 |
235 |
236 |
237 |
238 | false
239 |
240 |
241 | Export
242 |
243 |
244 | Ctrl+S
245 |
246 |
247 |
248 |
249 | false
250 |
251 |
252 | File information
253 |
254 |
255 | Ctrl+I
256 |
257 |
258 |
259 |
260 | false
261 |
262 |
263 | Copy
264 |
265 |
266 | Ctrl+C
267 |
268 |
269 |
270 |
271 | Quit
272 |
273 |
274 | Ctrl+Q
275 |
276 |
277 |
278 |
279 |
280 | ImageWidget
281 | QGraphicsView
282 |
283 |
284 |
285 |
286 |
287 |
288 | actionOpen
289 | triggered()
290 | MainWindow
291 | open()
292 |
293 |
294 | -1
295 | -1
296 |
297 |
298 | 399
299 | 299
300 |
301 |
302 |
303 |
304 | actionClose
305 | triggered()
306 | MainWindow
307 | close_file()
308 |
309 |
310 | -1
311 | -1
312 |
313 |
314 | 399
315 | 299
316 |
317 |
318 |
319 |
320 | actionQuit
321 | triggered()
322 | MainWindow
323 | close()
324 |
325 |
326 | -1
327 | -1
328 |
329 |
330 | 399
331 | 299
332 |
333 |
334 |
335 |
336 | actionAbout
337 | triggered()
338 | MainWindow
339 | about()
340 |
341 |
342 | -1
343 | -1
344 |
345 |
346 | 399
347 | 299
348 |
349 |
350 |
351 |
352 | actionFit_width
353 | triggered(bool)
354 | MainWindow
355 | fitToWindow(bool)
356 |
357 |
358 | -1
359 | -1
360 |
361 |
362 | 399
363 | 299
364 |
365 |
366 |
367 |
368 | actionNormal_size
369 | triggered()
370 | MainWindow
371 | normalSize()
372 |
373 |
374 | -1
375 | -1
376 |
377 |
378 | 399
379 | 299
380 |
381 |
382 |
383 |
384 | actionZoom_in
385 | triggered()
386 | MainWindow
387 | zoomIn()
388 |
389 |
390 | -1
391 | -1
392 |
393 |
394 | 399
395 | 299
396 |
397 |
398 |
399 |
400 | actionZoom_out
401 | triggered()
402 | MainWindow
403 | zoomOut()
404 |
405 |
406 | -1
407 | -1
408 |
409 |
410 | 399
411 | 299
412 |
413 |
414 |
415 |
416 | actionFile_information
417 | triggered()
418 | MainWindow
419 | show_file_info()
420 |
421 |
422 | -1
423 | -1
424 |
425 |
426 | 352
427 | 295
428 |
429 |
430 |
431 |
432 | actionSave
433 | triggered()
434 | MainWindow
435 | export()
436 |
437 |
438 | -1
439 | -1
440 |
441 |
442 | 352
443 | 295
444 |
445 |
446 |
447 |
448 | actionOpen_next
449 | triggered()
450 | MainWindow
451 | open_next_prev()
452 |
453 |
454 | -1
455 | -1
456 |
457 |
458 | 352
459 | 295
460 |
461 |
462 |
463 |
464 | actionOpen_previous
465 | triggered()
466 | MainWindow
467 | open_next_prev()
468 |
469 |
470 | -1
471 | -1
472 |
473 |
474 | 352
475 | 295
476 |
477 |
478 |
479 |
480 | actionCopy
481 | triggered()
482 | MainWindow
483 | copy_image_to_clipboard()
484 |
485 |
486 | -1
487 | -1
488 |
489 |
490 | 352
491 | 295
492 |
493 |
494 |
495 |
496 |
497 | open()
498 | open_next_prev()
499 | close_file()
500 | zoomIn()
501 | copy_image_to_clipboard()
502 | zoomOut()
503 | normalSize()
504 | fitToWindow(bool)
505 | about()
506 |
507 |
508 |
--------------------------------------------------------------------------------
/pimsviewer/pims_image.py:
--------------------------------------------------------------------------------
1 | import pims
2 | import numpy as np
3 | from PyQt5.QtCore import QDir, Qt, QSize, QRect, pyqtSignal, QPointF
4 | from PyQt5.QtGui import QImage, QPainter, QPalette, QPixmap, QPen
5 | from PyQt5.QtWidgets import (QAction, QApplication, QFileDialog, QLabel,
6 | QMainWindow, QMenu, QMessageBox, QScrollArea,
7 | QSizePolicy, QGraphicsPixmapItem)
8 |
9 | from pimsviewer.utils import pixmap_from_array, image_to_pixmap
10 |
11 |
12 | class PimsImage(QGraphicsPixmapItem):
13 |
14 | def __init__(self, parent):
15 | super(PimsImage, self).__init__()
16 |
17 | self.setAcceptHoverEvents(True)
18 |
19 | self.parent = parent
20 |
21 | def hoverMoveEvent(self, event):
22 | self.parent.hover_event.emit(event.lastPos())
23 |
24 | def array_to_pixmap(self, array):
25 | array = np.swapaxes(pims.to_rgb(array), 0, 1)
26 |
27 | image = pixmap_from_array(array)
28 |
29 | return image
30 |
31 |
--------------------------------------------------------------------------------
/pimsviewer/plugins.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from collections import deque
3 | from pims.display import to_rgb
4 | from os import path
5 |
6 | from PIL import Image, ImageQt
7 | from PyQt5.QtCore import QDir, Qt, QRectF
8 | from PyQt5.QtGui import QImage, QPainter, QPalette, QPixmap
9 | from PyQt5.QtWidgets import (QHBoxLayout, QSlider, QWidget, QAction, QApplication, QFileDialog, QLabel, QMainWindow, QMenu, QMessageBox, QScrollArea, QSizePolicy, QStatusBar, QVBoxLayout, QDockWidget, QPushButton, QStyle, QLineEdit, QDialog, QGraphicsEllipseItem)
10 |
11 | import pandas as pd
12 |
13 | from pimsviewer.utils import pixmap_from_array
14 |
15 | class Plugin(QDialog):
16 | name = 'Plugin'
17 | _active = False
18 |
19 | def __init__(self, parent=None):
20 | super(Plugin, self).__init__(parent)
21 | self.app = parent
22 |
23 | def activate(self):
24 | self.active = True
25 | self.show()
26 |
27 | @property
28 | def active(self):
29 | return self._active
30 |
31 | @active.setter
32 | def active(self, active):
33 | self._active = bool(active)
34 |
35 | def showFrame(self, image_widget):
36 | pass
37 |
--------------------------------------------------------------------------------
/pimsviewer/scroll_message_box.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import (QWidget, QDialog, QMessageBox, QScrollArea, QVBoxLayout, QLabel)
2 |
3 | class ScrollMessageBox(QMessageBox):
4 | def __init__(self, items, *args, **kwargs):
5 | super(ScrollMessageBox, self).__init__(*args, **kwargs)
6 |
7 | scroll = QScrollArea(self)
8 | scroll.setWidgetResizable(True)
9 |
10 | self.content = QWidget()
11 |
12 | scroll.setWidget(self.content)
13 |
14 | layout = QVBoxLayout(self.content)
15 | for item in items:
16 | layout.addWidget(QLabel(item, self))
17 |
18 | self.layout().addWidget(scroll, 0, 0, 1, self.layout().columnCount())
19 | self.setStyleSheet("QScrollArea{min-width:300 px; min-height: 400px}")
20 | self.show()
21 |
--------------------------------------------------------------------------------
/pimsviewer/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soft-matter/pimsviewer/9263ece121a58a0504c6e4d319ec6e18d1bb460a/pimsviewer/tests/__init__.py
--------------------------------------------------------------------------------
/pimsviewer/tests/test_gui.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import unittest
3 | from PyQt5.QtTest import QTest
4 | from PyQt5.QtCore import Qt
5 | from PyQt5.QtWidgets import QApplication
6 |
7 | from pimsviewer.gui import GUI
8 |
9 | class GuiTest(unittest.TestCase):
10 | app = None
11 | qapp = None
12 |
13 | def setUp(self):
14 | self.qapp = QApplication(sys.argv)
15 | self.app = GUI()
16 |
17 | def tearDown(self):
18 | self.app.close()
19 | self.qapp.exit()
20 |
21 | def test_init(self):
22 | self.assertEqual(self.app.windowTitle(), self.app.name)
23 |
24 | if __name__ == "__main__":
25 | unittest.main()
26 |
--------------------------------------------------------------------------------
/pimsviewer/tests/test_plugins.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from PyQt5.QtWidgets import QApplication
3 |
4 | import unittest
5 |
6 | from pimsviewer.gui import GUI
7 | from pimsviewer.example_plugins import ProcessingPlugin, AnnotatePlugin
8 |
9 | class PluginsTest(unittest.TestCase):
10 | def test_add_plugins(self):
11 | app = QApplication(sys.argv)
12 | gui = GUI(extra_plugins=[ProcessingPlugin, AnnotatePlugin])
13 |
14 | self.assertTrue(len(gui.plugins) > 1)
15 |
16 | has_processing_plugin = False
17 | has_annotate_plugin = False
18 | for p in gui.plugins:
19 | if isinstance(p, ProcessingPlugin):
20 | has_processing_plugin = True
21 | if isinstance(p, AnnotatePlugin):
22 | has_annotate_plugin = True
23 |
24 | self.assertTrue(has_processing_plugin)
25 | self.assertTrue(has_annotate_plugin)
26 | gui.close()
27 | app.exit()
28 |
29 | if __name__ == "__main__":
30 | unittest.main()
31 |
--------------------------------------------------------------------------------
/pimsviewer/utils.py:
--------------------------------------------------------------------------------
1 | from __future__ import (absolute_import, division, print_function,
2 | unicode_literals)
3 | import functools
4 | import numpy as np
5 | import pims
6 | from pims import to_rgb, normalize
7 | from pims.base_frames import FramesSequence, FramesSequenceND
8 | from pims.utils.sort import natural_keys
9 | from itertools import chain
10 | from os import listdir, path
11 | from os.path import isfile, join
12 |
13 | from PIL import Image, ImageQt
14 | from PyQt5.QtCore import Qt
15 | from PyQt5.QtGui import QImage, QPixmap
16 |
17 | def memoize(obj):
18 | """Memoize the function call result"""
19 | cache = obj.cache = {}
20 |
21 | @functools.wraps(obj)
22 | def memoizer(*args, **kwargs):
23 | """Memoize by storing the results in a dict"""
24 | key = str(args) + str(kwargs)
25 | if key not in cache:
26 | cache[key] = obj(*args, **kwargs)
27 | return cache[key]
28 |
29 | return memoizer
30 |
31 |
32 | def recursive_subclasses(cls):
33 | "Return all subclasses (and their subclasses, etc.)."
34 | # Source: http://stackoverflow.com/a/3862957/1221924
35 | return (cls.__subclasses__() +
36 | [g for s in cls.__subclasses__() for g in recursive_subclasses(s)])
37 |
38 |
39 | def drop_dot(s):
40 | if s.startswith('.'):
41 | return s[1:]
42 | else:
43 | return s
44 |
45 |
46 | @memoize
47 | def get_available_readers():
48 | readers = set(chain(recursive_subclasses(FramesSequence),
49 | recursive_subclasses(FramesSequenceND)))
50 | readers = [cls for cls in readers if not hasattr(cls, 'no_reader')]
51 | return readers
52 |
53 |
54 | def get_supported_extensions():
55 | # list all readers derived from the pims baseclasses
56 | all_handlers = chain(recursive_subclasses(FramesSequence),
57 | recursive_subclasses(FramesSequenceND))
58 | # keep handlers that support the file ext. use set to avoid duplicates.
59 | extensions = set(ext for h in all_handlers for ext in map(drop_dot, h.class_exts()))
60 |
61 | return extensions
62 |
63 | def get_all_files_in_dir(directory, extensions=None):
64 | if extensions is None:
65 | file_list = [f for f in listdir(directory) if isfile(join(directory, f))]
66 | else:
67 | file_list = [f for f in listdir(directory) if isfile(join(directory, f))
68 | and drop_dot(path.splitext(f)[1]) in extensions]
69 |
70 | return sorted(file_list, key=natural_keys)
71 |
72 | def pixmap_from_array(array):
73 | # Convert to image
74 | image = Image.fromarray(array)
75 | pixmap = ImageQt.toqpixmap(image)
76 |
77 | return pixmap
78 |
79 | def image_to_pixmap(image):
80 | flags = Qt.ImageConversionFlags(Qt.ColorOnly & Qt.DiffuseDither)
81 | pixmap = QPixmap.fromImage(image, flags)
82 |
83 | return pixmap
84 |
--------------------------------------------------------------------------------
/pimsviewer/wrapped_reader.py:
--------------------------------------------------------------------------------
1 | from pims import FramesSequenceND
2 | import numpy as np
3 |
4 | class WrappedReader(object):
5 | def __init__(self, reader):
6 | super(WrappedReader, self).__init__()
7 | self.reader = reader
8 |
9 | self._fallback_sizes = {}
10 | self._fallback_axis_order = {}
11 |
12 | def __getattr__(self, attr):
13 | if hasattr(self.reader, attr):
14 | value = getattr(self.reader, attr)
15 | self.setattr_only_self(attr, value)
16 | return value
17 | else:
18 | return self.get_fallback_function(attr)
19 |
20 | def setattr_only_self(self, attr, value):
21 | self.__dict__[attr] = value
22 |
23 | def __setattr__(self, attr, value):
24 | self.setattr_only_self(attr, value)
25 |
26 | if attr not in ['reader', '_fallback_sizes', '_fallback_axis_order']:
27 | setattr(self.reader, attr, value)
28 |
29 | def get_fallback_function(self, attr):
30 | if attr == 'sizes':
31 | return self.fallback_sizes
32 |
33 | if attr == 'default_coords':
34 | return self.fallback_def_coords
35 |
36 | raise AttributeError("Attribute '%s' not found in WrappedReader" % attr)
37 |
38 | def __getitem__(self, key):
39 | if isinstance(self.reader, FramesSequenceND):
40 | return self.reader[key]
41 | else:
42 | # provide a fallback for the FramesSequenceND behaviour
43 | frame = self.reader[0]
44 | index_values = []
45 | index_order = []
46 | for dim in self.sizes:
47 | if dim == 't':
48 | continue
49 |
50 | if dim in self.bundle_axes:
51 | index_values.append(slice(None))
52 | index_order.append(self.fallback_axis_order[dim])
53 | elif dim in self.iter_axes:
54 | index_values.append(key)
55 | index_order.append(self.fallback_axis_order[dim])
56 | else:
57 | index_values.append(self.fallback_def_coords[dim])
58 | index_order.append(self.fallback_axis_order[dim])
59 |
60 | index_order, index_values = (t for t in zip(*sorted(zip(index_order, index_values))))
61 |
62 | return frame[index_values]
63 |
64 | def __len__(self):
65 | return len(self.reader)
66 |
67 | def __iter__(self):
68 | return iter(self.reader[:])
69 |
70 | def __enter__(self):
71 | return self.reader
72 |
73 | def __exit__(self):
74 | self.close()
75 |
76 | def __repr__(self):
77 | return "<>" % str(self.reader)
78 |
79 | def close(self):
80 | try:
81 | self.reader.close()
82 | except AttributeError:
83 | return
84 |
85 | @property
86 | def fallback_axis_order(self):
87 | if len(self._fallback_axis_order) > 0:
88 | return self._fallback_axis_order
89 | _ = self.fallback_sizes
90 | # set inside fallback_sizes()
91 | return self._fallback_axis_order
92 |
93 | @property
94 | def fallback_sizes(self):
95 | if len(self._fallback_sizes) > 0:
96 | return self._fallback_sizes
97 |
98 | order = {}
99 | sizes = {}
100 |
101 | sizes['t'] = len(self.reader)
102 |
103 | frame_shape = self.reader.frame_shape
104 | to_process = np.arange(0, len(frame_shape), 1)
105 |
106 | if len(to_process) > 2:
107 | c_ix = np.argmin(frame_shape)
108 | sizes['c'] = frame_shape[c_ix]
109 | order['c'] = c_ix
110 | to_process = np.delete(to_process, c_ix)
111 |
112 | if len(to_process) == 3:
113 | sizes['z'] = frame_shape[-1]
114 | order['z'] = to_process[-1]
115 | to_process = np.delete(to_process, -1)
116 |
117 | sizes['y'] = frame_shape[0]
118 | order['y'] = 0
119 | sizes['x'] = frame_shape[1]
120 | order['x'] = 1
121 |
122 | self._fallback_sizes = sizes
123 | self._fallback_axis_order = order
124 |
125 | return sizes
126 |
127 | @property
128 | def fallback_def_coords(self):
129 | coords = self.fallback_sizes
130 | for dim in coords:
131 | coords[dim] = 0
132 | return coords
133 |
134 | @fallback_def_coords.setter
135 | def fallback_def_coords(self, value):
136 | pass
137 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soft-matter/pimsviewer/9263ece121a58a0504c6e4d319ec6e18d1bb460a/screenshot.png
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | from setuptools import setup
3 |
4 | try:
5 | descr = open(os.path.join(os.path.dirname(__file__), 'README.md')).read()
6 | except IOError:
7 | descr = ''
8 |
9 | try:
10 | from pypandoc import convert
11 |
12 | descr = convert(descr, 'rst', format='md')
13 | except ImportError:
14 | pass
15 |
16 | setup_parameters = dict(
17 | name="pimsviewer",
18 | version='2.0',
19 | description="Viewer for Python IMage Sequence (PIMS).",
20 | author="Ruben Verweij",
21 | author_email="ruben@lighthacking.nl",
22 | url="https://github.com/soft-matter/pimsviewer",
23 | install_requires=['click', 'pims', 'PyQt5>=5.13.1', 'pandas', 'numpy', 'Pillow'],
24 | python_requires='>=3.0',
25 | packages=['pimsviewer'],
26 | package_dir={'pimsviewer': 'pimsviewer'},
27 | package_data={'': ['*.ui']},
28 | long_description=descr,
29 | long_description_content_type="text/markdown",
30 | entry_points={
31 | 'gui_scripts': [
32 | 'pimsviewer=pimsviewer.gui:run',
33 | ],
34 | },
35 | )
36 |
37 | setup(**setup_parameters)
38 |
--------------------------------------------------------------------------------