Repository navigation
Port the visualizer to PyQt6 and fix its bugs - #74
Merged
Merged
Conversation
Use the scoped enum names, combine render hints with | rather than +, read mouse positions through position() since QMouseEvent.x() and y() are gone, and regenerate visualizer_ui.py with pyuic6 so that QAction comes from QtGui. Add an __init__.py and import the two helper modules through the package, so the visualizer runs as python -m storm_analysis.visualizer.visualizer from any directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getClosest compared the localizations with the click position plus half a pixel, but the circles are drawn at the click position minus half a pixel, so a click on the center of a circle found the localization one pixel down and to the right of it whenever there was one. This dates from the switch to zero based HDF5 coordinates, which updated the drawing but not the lookup. Also reset the selection index whenever the localization items are rebuilt, as the old index refers to the previous items. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every contrast change cleared the scene, which deleted the localization items, and read the frame from the movie again. The items came back from the localization file 100 ms later, so they flickered while the slider moved, and a click in that window raised a RuntimeError on the deleted items, which PyQt turns into an abort. The view now keeps the frame and a single pixmap item, and has separate methods for a new frame, a new contrast and new localizations. A contrast change only redraws the pixmap. As the scene is no longer cleared its rectangle is set from the frame size, so that it shrinks for a smaller movie. Equal contrast limits used to divide by zero, they now threshold the image. The frame is also drawn as an 8 bit grayscale image rather than being expanded to RGB32 first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PyQt aborts the program when a slot raises an exception that nothing catches, so choosing a movie or a localization file that couldn't be read closed the visualizer. Loading now reports the problem in a message box and keeps whatever was loaded before. A movie counts as readable once its first frame loads. For anything else, the program installs an exception hook that prints the exception and shows it rather than aborting. Also close the movie file when it is replaced and on exit, and show integer fields as integers whatever their width, not just int32. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A newly loaded movie is now zoomed to fill the view. Before, the view started at 2x and was reset to 1x on the first load, so a small movie was a small patch in the middle of the window and a large one needed scrolling. Frame Capture used to overwrite capture.png in whatever directory the visualizer was started from. It now opens a save dialog in the current data directory, suggesting the movie name and frame number, and reports the saved file and its size in the status bar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove the commented out key press signal and the keyPressEvent that only called the base class, an unused attribute, and a disabled debug print in the range slider. InfoTable is now a plain object as it only wraps a QTableWidget. MoleculeItem's pens are renamed so that they no longer hide QGraphicsEllipseItem.pen(). The visualizer README described PyQt4 and .bin files only. The other docs list PyQt6 as an optional dependency for the visualizer, and the note about making a Windows virtual environment that works with PyQt5 is gone. pyproject.toml gains a gui extra. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nine tests drive the GUI on Qt's offscreen platform: loading and
stepping through a movie, which localization a click selects, clicking
after a contrast change, equal contrast limits, files that can't be
read, closing a replaced movie, integer fields, fitting the view, and
frame capture. They need PyQt6 and the file is skipped without it.
Window takes an optional QSettings so that the tests can give it their
own file. QSettings.setDefaultFormat() and setPath() can't redirect
QSettings("Zhuang Lab", "visualizer"), which always uses the native
format, so without this the tests would read and overwrite the user's
own visualizer settings.
Update the test count in AGENTS.md, which was already behind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The slider ran from 0 to 100000, the limit of the spin boxes, so for a typical camera the useful range was a sliver at the bottom of it. It now runs from 0 to the smallest power of 2, and at least 256, that covers both the brightest pixel shown so far and the maximum contrast setting. Finding the brightest pixel costs one max() of each frame as it is shown, the movie is not read in advance. The range grows when a frame is brighter or a larger maximum is typed in, never shrinks while a movie is open so that the bars don't move as you step through it, and starts again with each movie. QRangeSlider.setRange() now updates the display, so the range can be changed after the slider is created. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The table had a fixed list of fields per file type, so fields from other programs never appeared, such as DECODE's score and uncertainties or the SMLM challenge's ground truth intensity, and fields a file didn't have were listed as "na". The rows now come from the selected localization: x, y and z first, then the rest in alphabetical order. The table is empty until something is selected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The visualizer opened HDF5 localization files with SAH5Py, which opens them for writing, so while they were loaded HDF5's file lock stopped any other program from reading them. It now uses SAH5Reader. isSAHDF5() also opened the file for writing just to check its attributes, which fails whenever another process has the file open. It then returned False, and the visualizer went on to read the HDF5 file as an Insight3 file. It now opens the file read only too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports
storm_analysis/visualizerfrom PyQt5 to PyQt6 (6.12), and fixes the problems found along the way.PyQt6: scoped enums,
|for render hints,QMouseEvent.position(),visualizer_ui.pyregenerated with pyuic6. The visualizer is now a package, sopython -m storm_analysis.visualizer.visualizerworks from any directory.Click selection: a click selected the localization one pixel down and to the right of the one under the mouse whenever there was one. This dates from the switch to zero-based HDF5 coordinates.
Contrast changes: these no longer clear the scene, so the circles stay put and a click right after a change no longer hits deleted items, which used to abort the program.
Errors: a movie or localization file that can't be read is reported in a message box instead of aborting. The program also installs an exception hook for anything else.
Smaller fixes:
Read-only HDF5: localization files are opened read-only (
SAH5Reader), so a loaded file no longer locks other programs out.sa_h5py.isSAHDF5()now opens files read-only as well. It used to open them read-write, so it returned False whenever another process had the file open.New behavior:
scoreor ground-truthintensity.Docs and packaging:
pyproject.tomlgains aguiextra (pip install .[gui]).Evidence
The new tests in
storm_analysis/test/test_visualizer.pydrive the GUI on Qt's offscreen platform. Most were written before their fix and seen to fail on the ported code:assert '11.000' == '10.000'(neighbor selected)RuntimeError: wrapped C/C++ object of type MoleculeItem has been deletedOSError: only .dax, .spe and .tif are supported(aborts the GUI)fileptrstill open'7.000' == '7'BlockingIOError: unable to lock fileOn
test.daxwithtest_ref.hdf5, 5 of 200 clicks on circle centers selected the wrong localization before the fix.The full suite gives 306 passed locally. The 12 visualizer tests need PyQt6, and the file is skipped without it. CI doesn't install PyQt6, so CI does not run them. I also checked the window on a real X display, and on the SMLM challenge AS-Exp movies with Spliner, DECODE and ground-truth HDF5 files.
Merge Danger
Door: two-way
Blast Radius: visualizer
The visualizer now requires PyQt6 and no longer runs on PyQt5.
multi_plane/mapper.py, which is deprecated, still uses PyQt5 and is unchanged. Outside the visualizer, the only change is thatisSAHDF5()opens files read-only. It gives the same answer, except that it now says True for a valid file another process has open, where it used to say False.fitting.pyis its only other caller.🤖 Generated with Claude Code