diff --git a/.bumpversion.cfg b/.bumpversion.cfg index dbfe1c52..d73370a4 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.6 +current_version = 0.9.7 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? @@ -15,10 +15,16 @@ serialize = [bumpversion:file:./picasso/version.py] +[bumpversion:file:./release/pyinstaller/picasso.spec] + +[bumpversion:file:./release/pyinstaller/picassow.spec] + [bumpversion:file:./release/one_click_windows_gui/picasso_innoinstaller.iss] [bumpversion:file:./release/one_click_windows_gui/create_installer_windows.bat] +[bumpversion:file:./release/one_click_macos_gui/create_macos_dmg.sh] + [bumpversion:file:./docs/conf.py] [bumpversion:file:pyproject.toml] diff --git a/.gitignore b/.gitignore index 3b3e37c4..9051ced9 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ picasso/config.yaml # One-click-installer output release/one_click_windows_gui/Output/ +release/one_click_macos_gui/*.dmg # PyInstaller # Usually these files are written by a python script from a template diff --git a/changelog.rst b/changelog.rst index e0adcb92..0bf10c08 100644 --- a/changelog.rst +++ b/changelog.rst @@ -1,7 +1,37 @@ Changelog ========= -Last change: 16-FEB-2026 CEST +Last change: 23-FEB-2026 CEST + +0.9.7 +----- +Important updates: +^^^^^^^^^^^^^^^^^^ +- Windows one-click-installer allows for selecting only a subset of Picasso modules to install +- Added ToRaw and Nanotron to one-click-installer +- *Experimental* One-click-installer for macOS (only for Apple Silicon), see `here `__ + +Small improvements: ++++++++++++++++++++ +- Adjusted the ``config.yaml`` and plugins instructions for the one-click-installer Picasso release (new Pyinstaller stores everything in the ``_internal`` folder) +- G5M output can save more columns (if present in the input localizations) +- Further enhancement of G5M documentation +- Render GUI: implemented filter by number of localizations for multichannel data +- Render GUI: allow removal of any column from localizations, not only ``group`` +- Filter GUI: allow removal of any column from localizations +- ``REQUIRED_COLUMNS`` moved from ``picasso.localize`` to ``picasso.lib`` + +Bug fixes: +++++++++++ +- Fixed basic frame analysis in SMLM clusterer +- Fixed labels of the vertical lines in the subcluster test plot +- Fixed automatic Localize loading/unloading z-calibration paths when changing cameras +- Fixed ``rel_sigma_z`` in G5M (previously incorrectly divided by pixel size) +- Fixed G5M molmap ``lpz`` output +- Fixed loading square picks in Render +- Fixed appearance of the Apply expression dialog in Render for files with many columns +- Fixed initial x, y and N in LQ Gaussian fitting (might results in faster convergence and slightly different (<< NeNA) results) (#616) +- Fixed picking circular regions around left and top edges of the FOV 0.9.6 ----- diff --git a/distribution/README.md b/distribution/README.md index eaed443e..537c4b3d 100644 --- a/distribution/README.md +++ b/distribution/README.md @@ -1,4 +1,7 @@ # Packaging Picasso and Creating an Installer + +**This folder will be removed in Picasso v0.11 as it is mostly a duplicate of picasso/release.** + This document describes the procedure to generate a Windows installer for Picasso end-users. The result is that Picasso (and Python) is installed in a single folder, the command line interface is exposed and start menu shortcuts are created. The first step is to package Picasso and its underlying Python distribution into a single folder. ## Requirements diff --git a/distribution/picasso.iss b/distribution/picasso.iss index 68eeb1b2..495f1ffe 100644 --- a/distribution/picasso.iss +++ b/distribution/picasso.iss @@ -2,10 +2,10 @@ AppName=Picasso AppPublisher=Jungmann Lab, Max Planck Institute of Biochemistry -AppVersion=0.9.6 +AppVersion=0.9.7 DefaultDirName={commonpf}\Picasso DefaultGroupName=Picasso -OutputBaseFilename="Picasso-Windows-64bit-0.9.6" +OutputBaseFilename="Picasso-Windows-64bit-0.9.7" ArchitecturesAllowed=x64 ArchitecturesInstallIn64BitMode=x64 diff --git a/docs/conf.py b/docs/conf.py index 7eaacf36..937707f4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,7 +26,7 @@ # The short X.Y version version = "" # The full version, including alpha/beta/rc tags -release = "0.9.6" +release = "0.9.7" # -- General configuration --------------------------------------------------- diff --git a/docs/localize.rst b/docs/localize.rst index bbcb0288..d7284a13 100644 --- a/docs/localize.rst +++ b/docs/localize.rst @@ -27,7 +27,43 @@ Identification and fitting of single-molecule spots Camera Config ------------- -Picasso can remember default cameras and will use saved camera parameters. In order to use camera configs, create a file named ``config.yaml`` in the picasso folder. To start with a template, modify ``config_template.yaml`` that can be found in the folder by default. Picasso will compare the entries with Micro-Manager-Metadata and match the sensitivity values. If no matching entries can be found (e.g., if the file was not created with Micro-Manager) the config file will still be used to create a dropdown menu to select the different categories. The camera config can also be used to define a default camera that will always be used. Indentions are used for definitions. +Picasso can remember default cameras and will use saved camera parameters. In order to use camera configs, create a file named ``config.yaml`` in the ``picasso`` folder. See below on how to locate it. + +To start with a template, modify ``config_template.yaml`` that can be found in the folder by default. Picasso will compare the entries with Micro-Manager-Metadata and match the sensitivity values. If no matching entries can be found (e.g., if the file was not created with Micro-Manager) the config file will still be used to create a dropdown menu to select the different categories. The camera config can also be used to define a default camera that will always be used. Indentions are used for definitions. + +One click installer (Windows) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you downloaded an .exe Picasso file from the `release page `_: + +- Navigate to the installation folder, by default, it's ``C:/Picasso``. *Before version 0.8.3, the default location was* ``C:/Program Files/Picasso``. +- Go to the folder ``_internal/picasso``. *Before version 0.9.6, the folder was simply* ``picasso``. +- Add your config file there. + +One click installer (macOS) +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you downloaded a .dmg Picasso file from the `release page `_: + +- Navigate to your Applications folder and right-click on the picasso app, then select "Show Package Contents". +- Go to the folder ``Contents/Frameworks/picasso``. +- Add your config file there. + +PyPI +~~~~ +If you installed Picasso using ``pip install picassosr``: + +- Activate your conda environment where ``picassosr`` is installed by typing ``conda activate YOUR_ENVIRONMENT``. +- To find the location of the package, type ``pip show picassosr`` and look for the line starting with ``Location:``. +- Navigate to this location and go to ``picasso``. +- Add your config file there. + +GitHub +~~~~~~ +If you cloned the GitHub repository, you can add plugins by following these steps: +- Find the directory where you cloned the GitHub repository with Picasso. +- Go to ``picasso/picasso/``. +- Copy the config file to this folder. Example: Default Camera ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/others.rst b/docs/others.rst index c8ccf91c..918a0a0c 100644 --- a/docs/others.rst +++ b/docs/others.rst @@ -28,10 +28,18 @@ If you installed Picasso using ``pip install picassosr``, you can add sound noti - Copy the sound files to this folder. -One click installer -------------------- +One click installer (Windows) +----------------------------- If you installed Picasso using the one click installer from `the Picasso release page `__ , you can add sound notifications by following these steps: - Find the location where you installed Picasso. By default, it is ``C:/Picasso``. *Before version 0.8.3, the default location was* ``C:/Program Files/Picasso``. - Go to the following subfolder: ``picasso/gui/notification_sounds``. -- Copy the sound files to this folder. \ No newline at end of file +- Copy the sound files to this folder. + + +One click installer (macOS) +--------------------------- +If you installed Picasso using the one click installer from `the Picasso release page `__ , you can add sound notifications by following these steps: + +- Navigate to your Applications folder and right-click on the picasso app, then select "Show Package Contents". +- Add your sound files to ``Contents/Frameworks/picasso/gui/notification_sounds``. \ No newline at end of file diff --git a/docs/plugins.rst b/docs/plugins.rst index f7636572..bbdd9224 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -8,16 +8,24 @@ Starting in version 0.5.0, Picasso supports plugins. Below are the instructions *Keep in mind that the* ``__init__.py`` *file in the* ``picasso/picasso/gui/plugins`` *folder must not be modified or deleted.* -One click installer -~~~~~~~~~~~~~~~~~~~ -**NOTE**: After uninstalling Picasso, ``Picasso`` folder needs to be deleted manually, as the uninstaller currently does not remove the plugins automatically. +One click installer (Windows) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +**NOTE**: After uninstalling Picasso, ``Picasso`` folder may need to be deleted manually, as the uninstaller currently does not remove the plugins automatically. - Find the location where you installed Picasso. By default, it is ``C:/Picasso``. *Before version 0.8.3, the default location was* ``C:/Program Files/Picasso``. -- Then go to the folder ``picasso/gui/plugins``. +- Then go to the folder ``_internal/picasso/gui/plugins``. *Before version 0.9.6, the folder was* ``/picasso/gui/plugins`` - Copy the plugin(s) to this folder. **NOTE**: Plugins added in this distribution will not be able to use packages that are not installed automatically (dependencies in the file ``pyproject.toml``). +One click installer (macOS) +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Navigate to your Applications folder and right-click on the picasso app, then select "Show Package Contents". +- Add your plugin(s) to ``Contents/Frameworks/picasso/gui/plugins``. + +**NOTE**: Plugins added in this distribution will not be able to use packages that are not installed automatically (dependencies in the file ``pyproject.toml``). + PyPI ~~~~ If you installed Picasso using ``pip install picassosr``, you can add plugins by following these steps: diff --git a/docs/render.rst b/docs/render.rst index 920ea5cb..23eaae33 100644 --- a/docs/render.rst +++ b/docs/render.rst @@ -106,7 +106,7 @@ If the outcome of G5M seems unsatisfactory, please check the following: - Another reason why the loc. precision values can be off is due to the small box size in the localization step; especially in 3D astigmatic imaging, single-emitter images can be quite large, potentially exceeding the user-defined box size; in such cases, we recommend increasing the box size in the localization step and rerunning the analysis; - Inspect if the localizations were preprocessed as described above; - Rerun the analysis without postprocessing (filtering) and redo it manually, since the step may be too stringent, especially for short acquisition times; -- Adjust min./max. σ, especially too low max. σ may lead to high false positive error rates (i.e., overfitting); +- Adjust min./max. σ, especially too low max. σ may lead to high false positive error rates (i.e., overfitting); We suggest inspecting ``rel_sigma`` values of the assigned molecules, which are calculated as the fitted σ divided by the mean localization precision of the surrounding localizations. If the values are close to the user-selected min./max. σ, min./max. σ might need to be adjusted. Alternatively, this might be a sign of inaccurate/inprecise loc. precision values, see above; - Adjust min. locs; - Adjust DBSCAN (or other clustering algorithm) parameters. For example, if G5M takes too long to run, the DBSCAN clusters most likely contain too many molecules. In such a case, we recommend splitting such clusters further; diff --git a/picasso/clusterer.py b/picasso/clusterer.py index 30500f7e..8a7add72 100644 --- a/picasso/clusterer.py +++ b/picasso/clusterer.py @@ -73,9 +73,7 @@ def _frame_analysis(frame: pd.SeriesGroupBy, n_frames: int) -> int: return passed -def frame_analysis( - labels: np.ndarray, frame: pd.Series | np.ndarray -) -> np.ndarray: +def frame_analysis(labels: np.ndarray, frame: np.ndarray) -> np.ndarray: """Perform basic frame analysis on clustered localizations. Reject clusters whose mean frame is outside of the [20, 80] % (max frame) range or any 1/20th of measurement's time contains more than 80 % of @@ -87,7 +85,7 @@ def frame_analysis( ---------- labels : np.ndarray Cluster labels (-1 means no cluster assigned). - frame : pd.Series or np.ndarray + frame : np.ndarray Frame number for each localization. Returns @@ -195,7 +193,8 @@ def _cluster( labels[np.isin(labels, to_discard)] = -1 if frame is not None: - labels = frame_analysis(labels, frame) + # must convert frames to an array, do not change! + labels = frame_analysis(labels, frame.to_numpy()) return labels @@ -956,7 +955,7 @@ def test_subclustering( sparse_dist: float = 80, ) -> tuple[np.ndarray, np.ndarray]: """Extract number of events from molecular maps based on their - numbers of binding events assinged. + numbers of binding events assigned. The reasoning is that 'subclustered' molecules will tend to have fewer binding events assigned to them since multiple molecules diff --git a/picasso/g5m.py b/picasso/g5m.py index b2f943dc..b6550711 100644 --- a/picasso/g5m.py +++ b/picasso/g5m.py @@ -1768,7 +1768,7 @@ def convert_G5M_results( sigma_x = np.sqrt(covariances[:, 0]) * pixelsize sigma_y = np.sqrt(covariances[:, 1]) * pixelsize sigma_z = np.sqrt(covariances[:, 2]) * pixelsize - lpz = sem[:, 2] + lpz = sem[:, 2] * pixelsize weighted_lpx = ( (resp * locs_group["lpx"].to_numpy().reshape(-1, 1)).sum(0) / rsum ).reshape(-1) @@ -1780,7 +1780,7 @@ def convert_G5M_results( ).reshape(-1) rel_sigma_x = sigma_x / weighted_lpx / pixelsize rel_sigma_y = sigma_y / weighted_lpy / pixelsize - rel_sigma_z = sigma_z / weighted_lpz / pixelsize + rel_sigma_z = sigma_z / weighted_lpz else: sigma = np.sqrt(covariances) * pixelsize # relative sigma @@ -1810,20 +1810,6 @@ def convert_G5M_results( log_likelihood = g5m.score_samples(X) locs_group["log_likelihood"] = log_likelihood - # photons, PSF size and background (weighted average) - photons = ( - (resp * locs_group["photons"].to_numpy().reshape(-1, 1)).sum(0) / rsum - ).reshape(-1) - sx = ( - (resp * locs_group["sx"].to_numpy().reshape(-1, 1)).sum(0) / rsum - ).reshape(-1) - sy = ( - (resp * locs_group["sy"].to_numpy().reshape(-1, 1)).sum(0) / rsum - ).reshape(-1) - bg = ( - (resp * locs_group["bg"].to_numpy().reshape(-1, 1)).sum(0) / rsum - ).reshape(-1) - # extract the number of binding events, i.e., link localizations # and assign them to molecules - sticky events will likely have only # one or two such events associated @@ -1861,10 +1847,6 @@ def convert_G5M_results( "x": x.astype(np.float32), "y": y.astype(np.float32), "z": z.astype(np.float32), - "photons": photons.astype(np.float32), - "sx": sx.astype(np.float32), - "sy": sy.astype(np.float32), - "bg": bg.astype(np.float32), "lpx": lpx.astype(np.float32), "lpy": lpy.astype(np.float32), "lpz": lpz.astype(np.float32), @@ -1889,10 +1871,6 @@ def convert_G5M_results( "std_frame": std_frame.astype(np.float32), "x": x.astype(np.float32), "y": y.astype(np.float32), - "photons": photons.astype(np.float32), - "sx": sx.astype(np.float32), - "sy": sy.astype(np.float32), - "bg": bg.astype(np.float32), "lpx": lpx.astype(np.float32), "lpy": lpy.astype(np.float32), "fitted_sigma": sigma.astype(np.float32), @@ -1905,6 +1883,25 @@ def convert_G5M_results( "group_input": group_input.astype(np.int32), } ) + # add mean values of extra columns from locs_group so that + # the info is not lost, e.g., mean photons + ignore_columns = [ + "frame", + "x", + "y", + "z", + "lpx", + "lpy", + "lpz", + "group", + "group_input", + ] + for col in locs_group.columns: + if col not in ignore_columns: + centers[f"{col}_mean"] = ( + (resp * locs_group[col].to_numpy().reshape(-1, 1)).sum(0) + / rsum + ).reshape(-1) return centers, locs_group diff --git a/picasso/gausslq.py b/picasso/gausslq.py index b5f9e71c..2a3964ec 100644 --- a/picasso/gausslq.py +++ b/picasso/gausslq.py @@ -50,16 +50,16 @@ def _sum_and_center_of_mass( size: int, ) -> tuple[float, float, float]: """Calculate the sum and center of mass of a 2D spot.""" - x = 0.0 y = 0.0 + x = 0.0 _sum_ = 0.0 for i in range(size): for j in range(size): - x += spot[i, j] * i - y += spot[i, j] * j + y += spot[i, j] * i + x += spot[i, j] * j _sum_ += spot[i, j] - x /= _sum_ y /= _sum_ + x /= _sum_ return _sum_, y, x diff --git a/picasso/gaussmle.py b/picasso/gaussmle.py index f31681e5..2ad480bb 100644 --- a/picasso/gaussmle.py +++ b/picasso/gaussmle.py @@ -30,16 +30,16 @@ def _sum_and_center_of_mass( size: int, ) -> tuple[float, float, float]: """Calculate the sum and center of mass of a 2D spot.""" - x = 0.0 y = 0.0 + x = 0.0 _sum_ = 0.0 for i in range(size): for j in range(size): - x += spot[i, j] * i - y += spot[i, j] * j + y += spot[i, j] * i + x += spot[i, j] * j _sum_ += spot[i, j] - x /= _sum_ y /= _sum_ + x /= _sum_ return _sum_, y, x diff --git a/picasso/gui/filter.py b/picasso/gui/filter.py index 0d8e8040..2032ebf4 100644 --- a/picasso/gui/filter.py +++ b/picasso/gui/filter.py @@ -621,7 +621,9 @@ def __init__(self) -> None: filter_menu = menu_bar.addMenu("Filter") filter_action = filter_menu.addAction("Filter") filter_action.setShortcut("Ctrl+F") - filter_menu.triggered.connect(self.filter_num.show) + filter_action.triggered.connect(self.filter_num.show) + remove_columns_action = filter_menu.addAction("Remove columns") + remove_columns_action.triggered.connect(self.remove_columns) main_widget = QtWidgets.QWidget() hbox = QtWidgets.QHBoxLayout(main_widget) hbox.setContentsMargins(0, 0, 0, 0) @@ -751,6 +753,21 @@ def log_filter(self, field: str, xmin: float, xmax: float) -> None: else: self.filter_log[field] = [xmin, xmax] + def remove_columns(self) -> None: + """Remove columns from the loaded dataset.""" + if self.locs is None: + return + columns = self.locs.columns.to_list() + to_remove, ok = lib.RemoveColumnsDialog.getParams(self, columns) + if not ok or len(to_remove) == 0: + return + self.locs.drop(columns=to_remove, inplace=True) + self.update_locs(self.locs) + if "Removed columns" in self.filter_log: + self.filter_log["Removed columns"].extend(to_remove) + else: + self.filter_log["Removed columns"] = to_remove + def save_file_dialog(self) -> None: if "x" in self.locs.columns: # Saving only for locs base, ext = os.path.splitext(self.locs_path) @@ -766,7 +783,7 @@ def save_file_dialog(self) -> None: info = self.info + [filter_info] io.save_locs(path, self.locs, info) else: - raise NotImplementedError("Saving only implmented for locs.") + raise NotImplementedError("Saving only implemented for locs.") def wheelEvent(self, event: QtGui.QWheelEvent) -> None: new_value = ( diff --git a/picasso/gui/localize.py b/picasso/gui/localize.py index 53f69ccd..a2d00a3e 100644 --- a/picasso/gui/localize.py +++ b/picasso/gui/localize.py @@ -656,6 +656,7 @@ def __init__(self, parent: QtWidgets.QMainWindow | None = None) -> None: self.setWindowTitle("Parameters") self.setModal(False) + self.z_calibration = {} self.z_calibration_path = None main_layout = QtWidgets.QVBoxLayout(self) @@ -1190,6 +1191,10 @@ def update_z_calib_with_config_path(self): # just because the calib file was loaded, uncheck the "Fit Z" # checkbox; self.fit_z_checkbox.setChecked(False) + else: + self.update_z_calib(None) + else: + self.update_z_calib(None) def update_z_calib(self, path: str) -> None: """Load the 3D calibration from a YAML file.""" @@ -1201,6 +1206,13 @@ def update_z_calib(self, path: str) -> None: self.z_calib_label.setText(os.path.basename(path)) self.fit_z_checkbox.setEnabled(True) self.fit_z_checkbox.setChecked(True) + else: + self.z_calibration = {} + self.z_calibration_path = None + self.z_calib_label.setAlignment(QtCore.Qt.AlignCenter) + self.z_calib_label.setText("-- no calibration loaded --") + self.fit_z_checkbox.setChecked(False) + self.fit_z_checkbox.setEnabled(False) def quality_progress(self, msg: str, index: int, result: str) -> None: """Update the quality progress UI elements.""" @@ -1461,7 +1473,7 @@ def __init__(self, parent: QtWidgets.QMainWindow) -> None: checkbox.setChecked(True) self.column_checkboxes[column] = checkbox vbox.addWidget(checkbox) - if column in localize.REQUIRED_COLUMNS: + if column in lib.REQUIRED_COLUMNS: checkbox.setDisabled(True) for key, column in localize.LOCALIZATION_COLUMNS.items(): if key == "Base": @@ -1471,7 +1483,7 @@ def __init__(self, parent: QtWidgets.QMainWindow) -> None: checkbox.setChecked(True) self.column_checkboxes[col] = checkbox vbox.addWidget(checkbox) - if col in localize.REQUIRED_COLUMNS: + if col in lib.REQUIRED_COLUMNS: checkbox.setDisabled(True) self.load_user_settings() diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 7c6a0e5b..fd2dd224 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -29,7 +29,6 @@ import yaml import matplotlib import matplotlib.pyplot as plt -import matplotlib.patches as patches import numpy as np import pandas as pd from matplotlib.backends.backend_qt5agg import FigureCanvas @@ -202,6 +201,22 @@ def wrapper(*args): return wrapper +def check_circular_picks(f: Callable) -> Callable: + """Decorator verifying if the picks are circular.""" + + def wrapper(*args): + if args[0]._pick_shape != "Circle": + QtWidgets.QMessageBox.warning( + args[0], + "Pick Error", + "This operation is only implemented for circular picks.", + ) + else: + return f(args[0]) + + return wrapper + + class FloatEdit(QtWidgets.QLineEdit): """Class used for adjusting the influx rate in the info dialog. @@ -376,6 +391,7 @@ def __init__(self, window: QtWidgets.QMainWindow) -> None: ) layout.addWidget(vars_label, 1, 0) self.label = QtWidgets.QLabel() + self.label.setWordWrap(True) layout.addWidget(self.label, 1, 1) self.update_vars(0) exp_label = QtWidgets.QLabel("Expression:") @@ -383,7 +399,12 @@ def __init__(self, window: QtWidgets.QMainWindow) -> None: "Enter the expression here.\n" "For example, 'x += 10' to move x coordinates of the\n" "localizations in the selected channel 10 camera pixels to the" - " right." + " right.\n" + "Additionally, the following commands are supported:\n" + "- 'flip x z' to exchange x- and z-axes.\n" + "- 'spiral R N' to plot each localization over time in a spiral\n" + " with radius R pixels and N turns.\n" + "- 'uspiral' to undo the last spiral action." ) layout.addWidget(exp_label, 2, 0) self.cmd = QtWidgets.QLineEdit() @@ -5603,12 +5624,7 @@ def initialize(self) -> None: def calculate_histogram(self) -> None: """Calculate the histograms of z coordinates of each channel.""" - # slice thickness - slice = self.pick_slice.value() - # ax = self.figure.add_subplot(111) - - # # clear the plot - # plt.cla() + slice_thickness = self.pick_slice.value() self.ax.clear() # get colors for each channel (from dataset dialog) @@ -5624,7 +5640,7 @@ def calculate_histogram(self) -> None: self.bins = np.arange( np.amin(np.hstack(self.zcoord)), np.amax(np.hstack(self.zcoord)), - slice, + slice_thickness, ) # plot histograms @@ -7596,7 +7612,12 @@ def dropEvent(self, event: QtGui.QDropEvent) -> None: # load pick regions if "Shape" in file: loaded_shape = file["Shape"] - if loaded_shape in ["Circle", "Rectangle", "Polygon"]: + if loaded_shape in [ + "Circle", + "Rectangle", + "Polygon", + "Square", + ]: self.load_picks(paths[0]) else: paths = [ @@ -7743,7 +7764,7 @@ def get_channel(self, title: str = "Choose a channel") -> int | None: else: return None - def save_channel( + def get_channel_save_locs( self, title: str = "Choose a channel to save localizations", ) -> int | None: @@ -7963,6 +7984,11 @@ def load_picks(self, path: str) -> None: self.window.tools_settings_dialog.pick_width.setValue(width) elif loaded_shape == "Polygon": self._picks = regions["Vertices"] + elif loaded_shape == "Square": + self._picks = regions["Centers"] + # no backward compatibility here, always in nm + width = regions["Side Length (nm)"] + self.window.tools_settings_dialog.pick_side_length.setValue(width) else: raise ValueError("Unrecognized pick shape") @@ -8003,6 +8029,8 @@ def load_screenshot(self, file: dict) -> None: elif "Scale bar length (nm)" in file: disp_dlg.scalebar.setValue(file["Scale bar length (nm)"]) + @check_picks + @check_circular_picks def subtract_picks(self, path: str) -> None: """Clear selected picks that cover the picks loaded from path. @@ -8016,12 +8044,8 @@ def subtract_picks(self, path: str) -> None: ValueError If .yaml file is not recognized. NotImplementedError - Rectangular picks have not been implemented yet. + Non-circular picks have not been implemented yet. """ - if self._pick_shape != "Circle": - raise NotImplementedError( - "Subtracting picks implemented for circular picks only." - ) oldpicks = self._picks.copy() pixelsize = self.window.display_settings_dlg.pixelsize.value() @@ -8567,11 +8591,10 @@ def select_traces(self) -> None: self.update_scene() @check_pick + @check_circular_picks def show_pick(self) -> None: """Let the user select picks based on their 2D scatter. Open ``self.pick_message_box`` to display information.""" - if self._pick_shape != "Circle": - raise NotImplementedError("Implemented for circular picks only.") channel = self.get_channel3d("Select Channel") removelist = [] # picks to be removed @@ -9037,107 +9060,139 @@ def analyze_cluster(self) -> None: self.update_scene() @check_picks + @check_circular_picks def filter_picks(self) -> None: """Filters picks by number of localizations.""" - channel = self.get_channel("Filter picks by locs") - if channel is not None: - locs = self.all_locs[channel] - d = self.window.tools_settings_dialog.pick_diameter.value() - r = d / 2 / self.window.display_settings_dlg.pixelsize.value() - # index locs in a grid - index_blocks = self.get_index_blocks(channel) + channel = self.get_channel_all_seq( + "Filter picks by number of localizations" + ) + if channel is None: + return - if self._picks: - removelist = [] # picks to remove - loccount = [] # n_locs in picks + # assumes circular picks + d = self.window.tools_settings_dialog.pick_diameter.value() + r = d / 2 / self.window.display_settings_dlg.pixelsize.value() + if channel is len(self.locs_paths): # all channels + channels = list(range(len(self.locs_paths))) + else: + channels = [channel] + # number of locs in each pick + loccount = np.zeros((len(channels), len(self._picks)), dtype=int) + for i, channel_ in enumerate(channels): + loccount[i] = self._count_locs_in_picks(channel_, r) + loccount = np.array(loccount) # shape (n_channels, n_picks) + + # plot histogram with n_locs in picks + fig = plt.figure(constrained_layout=True) + fig.canvas.manager.set_window_title("Localizations in Picks") + ax = fig.add_subplot(111) + ax.set_title("Localizations in Picks ") + + # get colors for each channel (from dataset dialog) + colors = [ + _.palette().color(QtGui.QPalette.Window) + for _ in self.window.dataset_dialog.colordisp_all + ] + colors = [ + [_.red() / 255, _.green() / 255, _.blue() / 255] for _ in colors + ] + bins = lib.calculate_optimal_bins(loccount.flatten(), max_n_bins=1000) + + for i, channel in enumerate(channels): + ax.hist( + loccount[i], + bins=bins, + density=False, + facecolor=colors[channel], + alpha=0.5, + ) + ax.set_xlabel("Number of localizations") + ax.set_ylabel("Counts") + fig.canvas.draw() + + # display the histogram instead of the rendered locs + width, height = fig.canvas.get_width_height() + im = QtGui.QImage( + fig.canvas.buffer_rgba(), + width, + height, + QtGui.QImage.Format_RGBA8888, + ) + self.setPixmap((QtGui.QPixmap(im))) + self.setAlignment(QtCore.Qt.AlignCenter) + + # filter picks by n_locs + removelist = [] + minlocs, ok = QtWidgets.QInputDialog.getInt( + self, + "Input Dialog", + "Enter minimum number of localizations:", + value=loccount.min(), + min=0, + max=loccount.max(), + ) + if ok: + maxlocs, ok2 = QtWidgets.QInputDialog.getInt( + self, + "Input Dialog", + "Enter maximum number of localizations:", + value=loccount.max(), + min=minlocs, + max=loccount.max(), + ) + if ok2: progress = lib.ProgressDialog( - "Counting in picks..", 0, len(self._picks) - 1, self + "Removing picks..", 0, len(self._picks) - 1, self ) progress.set_value(0) progress.show() - locs_xy = index_blocks[0][["x", "y"]].to_numpy().T for i, pick in enumerate(self._picks): - x, y = pick - # extract locs at a given region - numba version - block_locs_xy = postprocess.get_block_locs_at_numba( - int(x / r), - int(y / r), - locs_xy, - index_blocks[4], - index_blocks[5], - index_blocks[6], - index_blocks[7], - ) - pick_locs_xy = postprocess.locs_at_numba( - x, y, block_locs_xy, r - ) - loccount.append(pick_locs_xy.shape[1]) + for channel_idx, channel in enumerate(channels): + n_locs = loccount[channel_idx, i] + if n_locs > maxlocs or n_locs < minlocs: + removelist.append(pick) + # if one channel does not satisfy the condition, + # move on to the next pick + break progress.set_value(i) - progress.close() - - # plot histogram with n_locs in picks - fig = plt.figure(constrained_layout=True) - fig.canvas.manager.set_window_title("Localizations in Picks") - ax = fig.add_subplot(111) - ax.set_title("Localizations in Picks ") - n, bins, patches = ax.hist( - loccount, - bins="auto", - density=True, - facecolor="green", - alpha=0.75, - ) - ax.set_xlabel("Number of localizations") - ax.set_ylabel("Counts") - fig.canvas.draw() - - width, height = fig.canvas.get_width_height() - - # display the histogram instead of the rendered locs - im = QtGui.QImage( - fig.canvas.buffer_rgba(), - width, - height, - QtGui.QImage.Format_ARGB32, - ) - - self.setPixmap((QtGui.QPixmap(im))) - self.setAlignment(QtCore.Qt.AlignCenter) - - # filter picks by n_locs - minlocs, ok = QtWidgets.QInputDialog.getInt( - self, - "Input Dialog", - "Enter minimum number of localizations:", - ) - if ok: - maxlocs, ok2 = QtWidgets.QInputDialog.getInt( - self, - "Input Dialog", - "Enter maximum number of localizations:", - max(loccount), - minlocs, - ) - if ok2: - progress = lib.ProgressDialog( - "Removing picks..", 0, len(self._picks) - 1, self - ) - progress.set_value(0) - progress.show() - for i, pick in enumerate(self._picks): - - if loccount[i] > maxlocs: - removelist.append(pick) - elif loccount[i] < minlocs: - removelist.append(pick) - progress.set_value(i) + # adjust the attributes for pick in removelist: self._picks.remove(pick) self.n_picks = len(self._picks) self.update_pick_info_short() progress.close() - self.update_scene() + self.update_scene() + + def _count_locs_in_picks(self, channel: int, r: float) -> list[int]: + """Count number of localizations for each pick in a given + channel.""" + progress = lib.ProgressDialog( + "Counting in picks..", 0, len(self._picks) - 1, self + ) + progress.set_value(0) + progress.show() + loccount = np.zeros(len(self._picks), dtype=int) + # index locs in a grid + index_blocks = self.get_index_blocks(channel) + locs_xy = index_blocks[0][["x", "y"]].to_numpy().T + for i, pick in enumerate(self._picks): + x, y = pick + # extract locs at a given region - numba version + block_locs_xy = postprocess.get_block_locs_at_numba( + int(x / r), + int(y / r), + locs_xy, + index_blocks[4], + index_blocks[5], + index_blocks[6], + index_blocks[7], + ) + pick_locs_xy = postprocess.locs_at_numba(x, y, block_locs_xy, r) + loccount[i] = pick_locs_xy.shape[1] + progress.set_value(i) + progress.close() + return loccount def index_locs(self, channel: int, fast_render: bool = False) -> None: """Indexes localizations from a given channel in a grid with @@ -9221,6 +9276,7 @@ def pick_fiducials(self) -> None: self.add_picks(picks) @check_picks + @check_circular_picks def pick_similar(self) -> None: """Searche picks similar to the current picks. @@ -9233,10 +9289,6 @@ def pick_similar(self) -> None: NotImplementedError If pick shape is rectangle. """ - if self._pick_shape != "Circle": - raise NotImplementedError( - "Pick similar implemented for circular picks only." - ) channel = self.get_channel("Pick similar") if channel is not None: d = ( @@ -11599,8 +11651,8 @@ def initUI(self, plugins_loaded: bool) -> None: apply_drift_action.triggered.connect(self.view.apply_drift) postprocess_menu.addSeparator() - group_action = postprocess_menu.addAction("Remove group info") - group_action.triggered.connect(self.remove_group) + columns_action = postprocess_menu.addAction("Remove columns") + columns_action.triggered.connect(self.remove_columns) sync_group_action = postprocess_menu.addAction( "Synchronize groups across channels" ) @@ -12216,12 +12268,25 @@ def resizeEvent(self, even: QtGui.QResizeEvent) -> None: """Update window size.""" self.update_info() - def remove_group(self) -> None: - """Remove field 'group' from localizations.""" - channel = self.view.get_channel("Remove group") + def remove_columns(self) -> None: + """Remove user-selected columns from localizations.""" + channel = self.view.get_channel("Remove columns") if channel is not None: - self.view.locs[channel].drop(columns="group", inplace=True) - self.view.all_locs[channel].drop(columns="group", inplace=True) + columns = self.view.all_locs[channel].columns.to_list() + to_remove, ok = lib.RemoveColumnsDialog.getParams(self, columns) + if not ok or len(to_remove) == 0: + return + locs = self.view.all_locs[channel].copy() + info = self.view.infos[channel] + new_info = { + "Generated by": f"Picasso v{__version__} Remove columns", + "Removed columns": to_remove, + } + locs.drop(columns=to_remove, inplace=True) + + self.view.all_locs[channel] = locs + self.view.locs[channel] = locs.copy() + self.view.infos[channel] = info + [new_info] self.view.update_scene() def sync_groups(self) -> None: @@ -12269,7 +12334,7 @@ def save_pick_properties(self) -> None: def save_locs(self) -> None: """Save localizations in a given channel (or all channels).""" - channel = self.view.save_channel("Save localizations") + channel = self.view.get_channel_save_locs("Save localizations") if channel is not None: # combine all channels if channel is (len(self.view.locs_paths) + 1): @@ -12342,7 +12407,7 @@ def save_locs(self) -> None: def save_picked_locs(self) -> None: """Save picked localizations in a given channel (or all channels).""" - channel = self.view.save_channel("Save picked localizations") + channel = self.view.get_channel_save_locs("Save picked localizations") if channel is not None: # combine channels to one .hdf5 if channel is (len(self.view.locs_paths) + 1): @@ -12387,7 +12452,7 @@ def save_picked_locs(self) -> None: def save_picked_locs_separately(self) -> None: """Save picked localizations for each pick separately.""" - channel = self.view.save_channel( + channel = self.view.get_channel_save_locs( "Save picked localizations separately" ) if channel is not None: diff --git a/picasso/gui/rotation.py b/picasso/gui/rotation.py index 394e4816..24c7583f 100644 --- a/picasso/gui/rotation.py +++ b/picasso/gui/rotation.py @@ -2209,40 +2209,12 @@ def move_pick(self, dx: float, dy: float) -> None: self.window.view.update_scene() # update scene in main window - def save_channel_multi(self) -> int | None: - """Open an input dialog to ask which channel to save. There is - an option to save all channels. - - Returns - ------- - int or None - Index of the chosen channel. None, if no locs found or - channel picked are found. - """ - n_channels = len(self.view_rot.paths) - if n_channels == 0: - return None - elif n_channels == 1: - return 0 - elif len(self.view_rot.paths) > 1: - pathlist = list(self.view_rot.paths) - pathlist.append("Save all at once") - index, ok = QtWidgets.QInputDialog.getItem( - self, - "Save localizations", - "Channel:", - pathlist, - editable=False, - ) - if ok: - return pathlist.index(index) - else: - return None - def save_locs_rotated(self) -> None: """Save locs from the main window and provides rotation info for later loading.""" - channel = self.window.view.save_channel("Save rotated localizations") + channel = self.window.view.get_channel_save_locs( + "Save rotated localizations" + ) if channel is not None: # rotation info angx = int(self.view_rot.angx * 180 / np.pi) diff --git a/picasso/lib.py b/picasso/lib.py index f1479297..3c28e50b 100644 --- a/picasso/lib.py +++ b/picasso/lib.py @@ -43,6 +43,9 @@ # StatusDialog is finished SOUND_NOTIFICATION_DURATION = 60 # seconds +# Columns that are required for Picasso +REQUIRED_COLUMNS = ["frame", "x", "y", "z", "lpx", "lpy", "lpz"] + class ProgressDialog(QtWidgets.QProgressDialog): """ProgressDialog displays a progress dialog with a progress bar.""" @@ -266,6 +269,67 @@ def __init__(self, *args, **kwargs): super().__init__(AutoDict, *args, **kwargs) +class RemoveColumnsDialog(QtWidgets.QDialog): + """Allow the user to select columns to be removed from the locs + DataFrame.""" + + def __init__( + self, window: QtWidgets.QMainWindow, columns: list[str] + ) -> None: + super().__init__(window) + self.window = window + self.setWindowTitle("Remove columns") + self.setModal(True) + vbox = QtWidgets.QVBoxLayout(self) + self.setLayout(vbox) + self.checks = {} + for column in columns: + check = QtWidgets.QCheckBox(column) + check.setChecked(False) + if column in REQUIRED_COLUMNS: + check.setEnabled(False) + vbox.addWidget(check) + self.checks[column] = check + # OK and Cancel buttons + self.buttons = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, + QtCore.Qt.Horizontal, + self, + ) + vbox.addWidget(self.buttons) + self.buttons.accepted.connect(self.accept) + self.buttons.rejected.connect(self.reject) + + @staticmethod + def getParams( + parent: QtWidgets.QMainWindow, columns: list[str] + ) -> tuple[list[str], bool]: + """Open the dialog and return the columns to be removed. + + Parameters + ---------- + parent : QMainWindow + Instance of the main window. + columns : list of str + List of column names in the locs DataFrame. + + Returns + ------- + to_remove : list of str + List of column names to be removed. + accepted : bool + True if the user clicked OK, False if the user clicked + Cancel. + """ + dialog = RemoveColumnsDialog(parent, columns) + result = dialog.exec_() + to_remove = [] + for col in columns: + if dialog.checks[col].isChecked(): + to_remove.append(col) + return to_remove, result == QtWidgets.QDialog.Accepted + + def cancel_dialogs(): """Closes all open dialogs (``ProgressDialog`` and ``StatusDialog``) in the GUI.""" @@ -1234,10 +1298,10 @@ def plot_subclustering_check( Figure and axes if ``return_fig`` is True, otherwise (None, None). """ - m_far = clustered_n_events.mean() - m_close = sparse_n_events.mean() - s_far = clustered_n_events.std() - s_close = sparse_n_events.std() + m_close = clustered_n_events.mean() + m_far = sparse_n_events.mean() + s_close = clustered_n_events.std() + s_far = sparse_n_events.std() # create the plot fig, ax1 = plt.subplots(1, figsize=(6, 3), constrained_layout=True) @@ -1251,7 +1315,7 @@ def plot_subclustering_check( label=f"Clustered {m_close:.1f} +/- {s_close:.1f}", color="C0", ) - ax1.axvline(m_far, color="C0", linestyle="--") + ax1.axvline(m_close, color="C0", linestyle="--") vals, counts = np.unique(sparse_n_events, return_counts=True) ax1.bar( vals, @@ -1261,7 +1325,7 @@ def plot_subclustering_check( label=f"Sparse {m_far:.1f} +/- {s_far:.1f}", color="C1", ) - ax1.axvline(m_close, color="C1", linestyle="--") + ax1.axvline(m_far, color="C1", linestyle="--") ax1.set_xlabel("Number of events") ax1.set_ylabel("Counts") ax1.set_xlim(min_bin - 1, max_bin + 1) diff --git a/picasso/localize.py b/picasso/localize.py index bc185be3..b9c79b33 100755 --- a/picasso/localize.py +++ b/picasso/localize.py @@ -54,8 +54,6 @@ "Picked spots only": ["n_id"], "MLE only": ["log_likelihood", "iterations"], } -# Columns that are required for further use with Picasso -REQUIRED_COLUMNS = ["frame", "x", "y", "z", "lpx", "lpy", "lpz"] # For database: MEAN_COLS = LOCALIZATION_COLUMNS["Base"] + LOCALIZATION_COLUMNS["3D only"] SET_COLS = [ diff --git a/picasso/postprocess.py b/picasso/postprocess.py index ce0a8cdd..86468850 100644 --- a/picasso/postprocess.py +++ b/picasso/postprocess.py @@ -38,6 +38,8 @@ def get_index_blocks( """Split localizations into blocks of the given size. Used for fast localization indexing (e.g., for picking). + Note: this function will be moved to ``picasso.lib`` in Picasso v0.11.0 + Parameters ---------- locs : pd.DataFrame @@ -93,6 +95,8 @@ def index_blocks_shape(info: list[dict], size: float) -> tuple[int, int]: """Return the shape of the index grid, given the movie and grid sizes. + Note: this function will be moved to ``picasso.lib`` in Picasso v0.11.0 + Parameters ---------- info : list of dicts @@ -115,6 +119,8 @@ def get_block_locs_at(x: float, y: float, index_blocks: tuple) -> np.ndarray: """Return the localizations in the blocks around the given coordinates. + Note: this function will be moved to ``picasso.lib`` in Picasso v0.11.0 + Parameters ---------- x : float @@ -152,7 +158,10 @@ def _fill_index_blocks( y_index: np.ndarray, ) -> None: """Fill the block starts and ends arrays with the indices of - localizations in the blocks.""" + localizations in the blocks. + + Note: this function will be moved to ``picasso.lib`` in Picasso v0.11.0 + """ Y, X = block_starts.shape N = len(x_index) k = 0 @@ -174,7 +183,10 @@ def _fill_index_block( j: int, k: int, ) -> int: - """Fill the block starts and ends arrays for a single block.""" + """Fill the block starts and ends arrays for a single block. + + Note: this function will be moved to ``picasso.lib`` in Picasso v0.11.0 + """ block_starts[i, j] = k while k < N and y_index[k] == i and x_index[k] == j: k += 1 @@ -195,6 +207,8 @@ def picked_locs( """Find picked localizations, i.e., localizations within the given regions of interest. + Note: this function will be moved to ``picasso.lib`` in Picasso v0.11.0 + Parameters ---------- locs : pd.DataFrame @@ -391,6 +405,8 @@ def pick_similar( This function calls ``_pick_similar`` which is implemented in numba for speed. + Note: this function will be moved to ``picasso.lib`` in Picasso v0.11.0 + Parameters ---------- locs : pd.DataFrame @@ -522,6 +538,8 @@ def _pick_similar( ``pick_similar``. See that function for more user-friendly interface. + Note: this function will be moved to ``picasso.lib`` in Picasso v0.11.0 + Parameters ---------- x : np.ndarray @@ -638,7 +656,10 @@ def n_block_locs_at( block_ends: np.ndarray, ) -> int: """Return the number of localizations in the blocks around the - given coordinates.""" + given coordinates. + + Note: this function will be moved to ``picasso.lib`` in Picasso v0.11.0 + """ step = 0 for k in range(y_range - 1, y_range + 2): if 0 < k < K: @@ -666,12 +687,15 @@ def _get_block_locs_at_numba( L: int, ) -> np.ndarray: """Numba implementation of ``get_block_locs_at``. Return the indices - of localizations in the blocks around the given coordinates.""" + of localizations in the blocks around the given coordinates. + + Note: this function will be moved to ``picasso.lib`` in Picasso v0.11.0 + """ step = 0 for k in range(y_index - 1, y_index + 2): - if 0 < k < K: + if 0 <= k < K: for ll in range(x_index - 1, x_index + 2): - if 0 < ll < L: + if 0 <= ll < L: if block_ends[k, ll] - block_starts[k, ll] > 0: # numba does not work if you attach concatenate # to an empty list so the first step is @@ -708,7 +732,10 @@ def get_block_locs_at_numba( L: int, ) -> np.ndarray: """Numba implementation of ``get_block_locs_at. Return the - localizations in the blocks around the given coordinates.""" + localizations in the blocks around the given coordinates. + + Note: this function will be moved to ``picasso.lib`` in Picasso v0.11.0 + """ indices = _get_block_locs_at_numba( x_index, y_index, @@ -728,7 +755,9 @@ def _locs_at_numba( r: float, ) -> np.ndarray: """Numba implementation of ``lib.locs_at``. Return the indices of - localizations at the given coordinates within radius ``r``.""" + localizations at the given coordinates within radius ``r``. + + Note: this function will be moved to ``picasso.lib`` in Picasso v0.11.0""" dx = locs_xy[0] - x dy = locs_xy[1] - y r2 = r**2 @@ -745,7 +774,10 @@ def locs_at_numba( r: float, ) -> np.ndarray: """Numba implementation of ``lib.locs_at``. Return the localizations - at the given coordinates within radius ``r``.""" + at the given coordinates within radius ``r``. + + Note: this function will be moved to ``picasso.lib`` in Picasso v0.11.0 + """ is_picked = _locs_at_numba(x, y, locs_xy, r) return locs_xy[:, is_picked] @@ -753,7 +785,9 @@ def locs_at_numba( @numba.jit(nopython=True, nogil=True) def rmsd_at_com(locs_xy: np.ndarray) -> float: """Calculate the RMSD of the localizations at the center of mass - (COM) of the localizations.""" + (COM) of the localizations. + + Note: this function will be moved to ``picasso.lib`` in Picasso v0.11.0""" com_x = np.mean(locs_xy[0]) com_y = np.mean(locs_xy[1]) return np.sqrt( diff --git a/picasso/version.py b/picasso/version.py index 50533e30..f5b77301 100644 --- a/picasso/version.py +++ b/picasso/version.py @@ -1 +1 @@ -__version__ = "0.9.6" +__version__ = "0.9.7" diff --git a/pyproject.toml b/pyproject.toml index 34c6d1ba..71549450 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "picassosr" -version = "0.9.6" +version = "0.9.7" authors = [ {name = "Joerg Schnitzbauer", email = "joschnitzbauer@gmail.com"}, {name = "Maximilian T. Strauss", email = "straussmaximilian@gmail.com"}, @@ -43,7 +43,6 @@ dependencies = [ "playsound3==3.2.8", "imageio==2.37.0", "imageio-ffmpeg==0.6.0", - "PyImarisWriter==0.7.0; sys_platform=='win32'", ] [project.optional-dependencies] diff --git a/release/logos/average.icns b/release/logos/average.icns new file mode 100644 index 00000000..f31bee17 Binary files /dev/null and b/release/logos/average.icns differ diff --git a/release/logos/design.icns b/release/logos/design.icns new file mode 100644 index 00000000..09511914 Binary files /dev/null and b/release/logos/design.icns differ diff --git a/release/logos/filter.icns b/release/logos/filter.icns new file mode 100644 index 00000000..f531bd6d Binary files /dev/null and b/release/logos/filter.icns differ diff --git a/release/logos/localize.icns b/release/logos/localize.icns new file mode 100644 index 00000000..ea3064ad Binary files /dev/null and b/release/logos/localize.icns differ diff --git a/release/logos/nanotron.icns b/release/logos/nanotron.icns new file mode 100644 index 00000000..92eb2b75 Binary files /dev/null and b/release/logos/nanotron.icns differ diff --git a/release/logos/render.icns b/release/logos/render.icns new file mode 100644 index 00000000..2b45fb2d Binary files /dev/null and b/release/logos/render.icns differ diff --git a/release/logos/server.icns b/release/logos/server.icns new file mode 100644 index 00000000..62904c1c Binary files /dev/null and b/release/logos/server.icns differ diff --git a/release/logos/simulate.icns b/release/logos/simulate.icns new file mode 100644 index 00000000..ddd18b49 Binary files /dev/null and b/release/logos/simulate.icns differ diff --git a/release/logos/spinna.icns b/release/logos/spinna.icns new file mode 100644 index 00000000..b715420f Binary files /dev/null and b/release/logos/spinna.icns differ diff --git a/release/logos/toraw.icns b/release/logos/toraw.icns new file mode 100644 index 00000000..7396448b Binary files /dev/null and b/release/logos/toraw.icns differ diff --git a/release/one_click_macos_gui/create_macos_dmg.sh b/release/one_click_macos_gui/create_macos_dmg.sh new file mode 100644 index 00000000..6b331141 --- /dev/null +++ b/release/one_click_macos_gui/create_macos_dmg.sh @@ -0,0 +1,266 @@ +#!/bin/bash +# ============================================================================= +# create_macos_dmg.sh +# Builds a macOS DMG installer for Picasso +# Requirements: PyInstaller, create-dmg (brew install create-dmg) +# Usage: bash create_macos_dmg.sh +# ============================================================================= + +set -e # Exit immediately on any error + +# Initialize conda for this shell session +eval "$(conda shell.bash hook)" + +APP_NAME="Picasso" +VERSION="0.9.7" +MAIN_BUNDLE_NAME="Picasso.app" +DMG_NAME="Picasso-v$VERSION-macOS-Apple-Silicon" +PYINSTALLER_FILE="../pyinstaller/picasso_pyinstaller.py" +DIST_DIR="../pyinstaller/dist" +BUILD_DIR="../pyinstaller/build" +STAGING_DIR="macos_dmg_staging" +APPS_LINK="/Applications" +# Tool definitions: name, argument, icon filename (without .icns extension) +declare -a TOOLS=( + "Design:design:design" + "Simulate:simulate:simulate" + "Localize:localize:localize" + "Filter:filter:filter" + "Render:render:render" + "Average:average:average" + "SPINNA:spinna:spinna" + "Server:server:server" + "Nanotron:nanotron:nanotron" + "Toraw:toraw:toraw" +) + +# ----------------------------------------------------------------------------- +# Step 0: Create a conda environment and prepare the package +# ----------------------------------------------------------------------------- +echo ">>> Setting up conda environment and preparing package..." +# Create conda environment (if not already created) +echo "Creating conda environment 'installer'..." +conda create -n installer python=3.10.19 -y +conda activate installer +pip install build +cd ../.. +python -m build +pip install dist/picassosr-$VERSION-py3-none-any.whl +pip install pyinstaller==6.19 +cd release/one_click_macos_gui + +# ----------------------------------------------------------------------------- +# Step 1: Run PyInstaller to build the main .app bundle +# ----------------------------------------------------------------------------- +# echo ">>> Building main .app bundle with PyInstaller..." +pyinstaller "$PYINSTALLER_FILE" \ + --onedir \ + --windowed \ + --collect-all picasso \ + --name picasso \ + --icon ../logos/localize.icns \ + --distpath "$DIST_DIR" \ + --workpath "$BUILD_DIR" \ + --noconfirm + +MAIN_APP_PATH="$DIST_DIR/$MAIN_BUNDLE_NAME" + +if [ ! -d "$MAIN_APP_PATH" ]; then + echo "ERROR: PyInstaller did not produce $MAIN_APP_PATH" + exit 1 +fi +echo ">>> Main .app bundle created at $MAIN_APP_PATH" + +# Get the path to the main executable +MAIN_EXECUTABLE="$MAIN_APP_PATH/Contents/MacOS/picasso" +if [ ! -f "$MAIN_EXECUTABLE" ]; then + echo "ERROR: Main executable not found at $MAIN_EXECUTABLE" + exit 1 +fi + +# Get the resources directory for icons +MAIN_RESOURCES="$MAIN_APP_PATH/Contents/Resources" + +# --------------------------------------------------------------------------- +# Step 1b: Fix HDF5 library conflict (h5py 3.15.1 vs tables 3.10.1), which +# happened at version 0.9..6 +# Both h5py and tables ship their own libhdf5.310.dylib from different HDF5 +# versions. PyInstaller may bundle the older (tables) copy, causing a symbol +# mismatch at runtime. Force h5py's versions into the bundle. +# --------------------------------------------------------------------------- +H5PY_DYLIBS=$(python3 -c "import h5py, os; print(os.path.join(os.path.dirname(h5py.__file__), '.dylibs'))" 2>/dev/null || true) +if [ -d "$H5PY_DYLIBS" ]; then + FRAMEWORKS_DIR="$MAIN_APP_PATH/Contents/Frameworks" + # Fall back to _internal for --onedir non-macOS-bundle layouts + if [ ! -d "$FRAMEWORKS_DIR" ]; then + FRAMEWORKS_DIR="$MAIN_APP_PATH/Contents/MacOS/_internal" + fi + if [ -d "$FRAMEWORKS_DIR" ]; then + echo ">>> Fixing HDF5 libraries: copying h5py's dylibs into bundle..." + for dylib in "$H5PY_DYLIBS"/libhdf5*.dylib; do + if [ -f "$dylib" ]; then + cp -f "$dylib" "$FRAMEWORKS_DIR/" + echo " Copied $(basename "$dylib")" + fi + done + fi +fi + +# ----------------------------------------------------------------------------- +# Step 2: Create separate .app bundles for each tool +# ----------------------------------------------------------------------------- +echo ">>> Creating individual .app bundles for each tool..." + +for tool_def in "${TOOLS[@]}"; do + IFS=':' read -r display_name argument icon_name <<< "$tool_def" + + app_name="Picasso ${display_name}.app" + app_path="$DIST_DIR/$app_name" + + echo " Creating $app_name..." + + # Create .app bundle structure + mkdir -p "$app_path/Contents/MacOS" + mkdir -p "$app_path/Contents/Resources" + + # Create launcher script that calls the main executable with the tool argument + launcher_script="$app_path/Contents/MacOS/launcher" + cat > "$launcher_script" < "$app_path/Contents/Info.plist" < + + + + CFBundleExecutable + launcher + CFBundleIconFile + icon.icns + CFBundleIdentifier + org.jungmannlab.picasso.${argument} + CFBundleName + Picasso ${display_name} + CFBundleDisplayName + Picasso ${display_name} + CFBundleShortVersionString + ${VERSION} + CFBundleVersion + ${VERSION} + CFBundlePackageType + APPL + NSHighResolutionCapable + + NSPrincipalClass + NSApplication + + +EOF +done + +echo ">>> Individual tool bundles created successfully" + +# ----------------------------------------------------------------------------- +# Step 3: Stage the DMG contents +# ----------------------------------------------------------------------------- +echo ">>> Staging DMG contents..." + +rm -rf "$STAGING_DIR" +mkdir -p "$STAGING_DIR" + +# Copy the main .app bundle +cp -r "$MAIN_APP_PATH" "$STAGING_DIR/$MAIN_BUNDLE_NAME" + +# Copy all tool-specific .app bundles +for tool_def in "${TOOLS[@]}"; do + IFS=':' read -r display_name argument icon_name <<< "$tool_def" + app_name="Picasso ${display_name}.app" + cp -r "$DIST_DIR/$app_name" "$STAGING_DIR/$app_name" +done + +# Note: Applications symlink is created automatically by create-dmg via --app-drop-link + +# ----------------------------------------------------------------------------- +# Step 4: Build the DMG with create-dmg +# ----------------------------------------------------------------------------- +echo ">>> Building DMG with create-dmg..." + +# Remove any previous DMG +rm -f "${DMG_NAME}.dmg" + +# Build the DMG +# Note: With multiple apps, we'll let create-dmg handle the layout automatically +# or you can manually position each icon if you prefer more control +create-dmg \ + --volname "$APP_NAME $VERSION" \ + --volicon "../logos/localize.icns" \ + --window-pos 200 120 \ + --window-size 800 500 \ + --icon-size 80 \ + --text-size 12 \ + --app-drop-link 650 250 \ + --no-internet-enable \ + "${DMG_NAME}.dmg" \ + "$STAGING_DIR" + +# ----------------------------------------------------------------------------- +# Step 5: Cleanup staging directory +# ----------------------------------------------------------------------------- +echo ">>> Cleaning up staging directory..." +rm -rf "$STAGING_DIR" + +echo "" +echo "=============================================" +echo " Build complete!" +echo " Output: ${DMG_NAME}.dmg" +echo "" +echo " The DMG contains:" +echo " - Picasso.app (main bundle)" +echo " - Picasso Design.app" +echo " - Picasso Simulate.app" +echo " - Picasso Localize.app" +echo " - Picasso Filter.app" +echo " - Picasso Render.app" +echo " - Picasso Average.app" +echo " - Picasso SPINNA.app" +echo " - Picasso Server.app" +echo " - Picasso Nanotron.app" +echo " - Picasso Toraw.app" +echo "=============================================" + +# Delete the conda environment +conda deactivate +conda remove -n installer --all -y +# Delete the .spec file generated by PyInstaller +rm -f picasso.spec diff --git a/release/one_click_macos_gui/readme.rst b/release/one_click_macos_gui/readme.rst new file mode 100644 index 00000000..be824cfa --- /dev/null +++ b/release/one_click_macos_gui/readme.rst @@ -0,0 +1,76 @@ +One-click installer for macOS +============================= + +This is the one-click installer for Picasso on macOS. The Picasso software is complemented by our `Nature Protocols publication `__. + +A comprehensive documentation can be found here: `Read the Docs `__. + +How to install +-------------- + +1. Download the latest release from the `release page `__. +2. Open the downloaded dmg file and drag the "picasso" icon to your Applications folder first. +3. Repeat the same for all other icons with separate Picasso modules (Localize, Render, etc) that you wish to use. +4. **Note:** The main picasso app must remain in the Applications folder, the shortcuts for the other modules can be moved to the desktop or elsewhere if desired. + +Picasso is distributed without an Apple Developer ID, which means that macOS may block the installation of the software. If the steps above do not work, please follow the instructions under the `link `__. Alternatively, you can create the dmg file yourself by cloning our GitHub repo and running the bash script ``picasso/release/one_click_macos_gui/create_macos_dmg.sh`` from the Terminal. Note that you must have conda installed on your computer. + +Adding camera configuration and plugins +--------------------------------------- + +Camera configuration is essential for correct photon conversion and thus correct localization precision calculation. For more details, see `documentation `__. + +To add your config.yaml file, navigate to your Applications folder and right-click on the picasso app, then select "Show Package Contents". Add your config file to Contents/Frameworks/picasso. + +Similarly, you can add Picasso plugins under the folder Contents/Frameworks/picasso/gui/plugins. For more details on how to create plugins, see `documentation `__. + +Changelog +--------- +To see all changes introduced across releases, see `here `_. + +Contributions & Copyright +------------------------- + +| Contributors: Joerg Schnitzbauer, Maximilian Strauss, Rafal Kowalewski, Adrian Przybylski, Andrey Aristov, Hiroshi Sasaki, Alexander Auer, Johanna Rahm +| Copyright (c) 2015-2025 Jungmann Lab, Max Planck Institute of Biochemistry +| Copyright (c) 2020-2021 Maximilian Strauss + +Citing Picasso +-------------- + +If you use Picasso in your research, please cite our Nature Protocols publication describing the software. + +| J. Schnitzbauer*, M.T. Strauss*, T. Schlichthaerle, F. Schueder, R. Jungmann +| Super-Resolution Microscopy with DNA-PAINT +| Nature Protocols (2017). 12: 1198-1228 DOI: `10.1038/nprot.2017.024 `__ +| +| If you use some of the functionalities provided by Picasso, please also cite the respective publications: + +- NeNA. DOI: `10.1007/s00418-014-1192-3 `__ +- FRC. DOI: `10.1038/nmeth.2448 `__ +- Theoretical lateral localization precision (Gauss LQ and MLE). DOI: `10.1038/nmeth.1447 `__ +- Theoretical axial localization precision (Gauss LQ and MLE). DOI: *DOI will be added once available* +- MLE fitting. DOI: `10.1038/nmeth.1449 `__ +- RCC undrifting: DOI: `10.1364/OE.22.015982 `__ +- AIM undrifting. DOI: `10.1126/sciadv.adm776 `__ +- SMLM clusterer. DOIs: `10.1038/s41467-021-22606-1 `__ and `10.1038/s41586-023-05925-9 `__ +- DBSCAN: Ester, et al. Inkdd, 1996. (Vol. 96, No. 34, pp. 226-231). +- HDBSCAN. DOI: `10.1007/978-3-642-37456-2_14 `__ +- RESI. DOI: `10.1038/s41586-023-05925-9 `__ +- Nanotron. DOI: `10.1093/bioinformatics/btaa154 `__ +- Picasso: Server. DOI: `10.1038/s42003-022-03909-5 `__ +- SPINNA. DOI: `10.1038/s41467-025-59500-z `__ +- SPINNA for LE fitting. DOI: `10.1038/s41592-024-02242-5 `__ +- G5M. DOI: *DOI will be added once available* + +Credits +------- + +- Design icon based on “Hexagon by Creative Stalls" from the Noun Project +- Simulate icon based on “Microchip by Futishia" from the Noun Project +- Localize icon based on “Mountains" by MONTANA RUCOBO from the Noun Project +- Filter icon based on “Funnel" by José Campos from the Noun Project +- Render icon based on “Paint Palette" by Vectors Market from the Noun Project +- Average icon based on “Layers" by Creative Stall from the Noun Project +- Server icon based on “Database" by Nimal Raj from the Noun Project +- SPINNA icon based on "Spinner" by Viktor Ostrovsky from the Noun Project diff --git a/release/one_click_windows_gui/create_installer_windows.bat b/release/one_click_windows_gui/create_installer_windows.bat index 9747d6c5..938f010c 100644 --- a/release/one_click_windows_gui/create_installer_windows.bat +++ b/release/one_click_windows_gui/create_installer_windows.bat @@ -12,7 +12,7 @@ call pip install build call python -m build call cd release/one_click_windows_gui -call pip install "../../dist/picassosr-0.9.6-py3-none-any.whl" +call pip install "../../dist/picassosr-0.9.7-py3-none-any.whl" call pip install pyinstaller==6.19.0 call pyinstaller ../pyinstaller/picasso.spec -y --clean @@ -20,12 +20,10 @@ call pyinstaller ../pyinstaller/picassow.spec -y --clean call conda deactivate call conda remove -n picasso_installer --all -y -call robocopy ../../picasso dist/picasso/picasso /E call robocopy ../../picasso dist/picasso/_internal/picasso /E call robocopy ../../picasso dist/picassow/_internal/picasso /E copy dist\picassow\picassow.exe dist\picasso\picassow.exe copy dist\picassow\picassow.exe.manifest dist\picasso\picassow.exe.manifest - call "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" picasso_innoinstaller.iss \ No newline at end of file diff --git a/release/one_click_windows_gui/picasso_innoinstaller.iss b/release/one_click_windows_gui/picasso_innoinstaller.iss index c8d27471..e5f73fcb 100644 --- a/release/one_click_windows_gui/picasso_innoinstaller.iss +++ b/release/one_click_windows_gui/picasso_innoinstaller.iss @@ -1,35 +1,55 @@ [Setup] AppName=Picasso AppPublisher=Jungmann Lab, Max Planck Institute of Biochemistry -AppVersion=0.9.6 +AppVersion=0.9.7 DefaultDirName="C:\Picasso" DefaultGroupName=Picasso -OutputBaseFilename="Picasso-Windows-64bit-0.9.6" +OutputBaseFilename="Picasso-Windows-64bit-0.9.7" ArchitecturesAllowed=x64 ArchitecturesInstallIn64BitMode=x64 [Files] Source: "dist\picasso\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs +[Types] +Name: "full"; Description: "Full installation" +Name: "custom"; Description: "Custom installation"; Flags: iscustom + +[Components] +Name: "design"; Description: "Design"; Types: full custom +Name: "localize"; Description: "Localize"; Types: full custom +Name: "simulate"; Description: "Simulate"; Types: full custom +Name: "filter"; Description: "Filter"; Types: full custom +Name: "render"; Description: "Render"; Types: full custom +Name: "average"; Description: "Average"; Types: full custom +Name: "spinna"; Description: "SPINNA"; Types: full custom +Name: "server"; Description: "Server"; Types: full custom +Name: "nanotron"; Description: "Nanotron"; Types: full custom +Name: "toraw"; Description: "ToRaw"; Types: full custom + [Icons] -Name: "{group}\Design"; Filename: "{app}\picassow.exe"; Parameters: "design"; IconFilename: "{app}\picasso\gui\icons\design.ico" -Name: "{group}\Simulate"; Filename: "{app}\picassow.exe"; Parameters: "simulate"; IconFilename: "{app}\picasso\gui\icons\simulate.ico" -Name: "{group}\Localize"; Filename: "{app}\picassow.exe"; Parameters: "localize"; IconFilename: "{app}\picasso\gui\icons\localize.ico" -Name: "{group}\Filter"; Filename: "{app}\picassow.exe"; Parameters: "filter"; IconFilename: "{app}\picasso\gui\icons\filter.ico" -Name: "{group}\Render"; Filename: "{app}\picassow.exe"; Parameters: "render"; IconFilename: "{app}\picasso\gui\icons\render.ico" -Name: "{group}\Average"; Filename: "{app}\picassow.exe"; Parameters: "average"; IconFilename: "{app}\picasso\gui\icons\average.ico" -Name: "{group}\SPINNA"; Filename: "{app}\picassow.exe"; Parameters: "spinna"; IconFilename: "{app}\picasso\gui\icons\spinna.ico" -Name: "{group}\Server"; Filename: "{app}\picasso.exe"; Parameters: "server"; IconFilename: "{app}\picasso\gui\icons\server.ico" - - -Name: "{autodesktop}\Design"; Filename: "{app}\picassow.exe"; Parameters: "design"; IconFilename: "{app}\picasso\gui\icons\design.ico" -Name: "{autodesktop}\Simulate"; Filename: "{app}\picassow.exe"; Parameters: "simulate"; IconFilename: "{app}\picasso\gui\icons\simulate.ico" -Name: "{autodesktop}\Localize"; Filename: "{app}\picassow.exe"; Parameters: "localize"; IconFilename: "{app}\picasso\gui\icons\localize.ico" -Name: "{autodesktop}\Filter"; Filename: "{app}\picassow.exe"; Parameters: "filter"; IconFilename: "{app}\picasso\gui\icons\filter.ico" -Name: "{autodesktop}\Render"; Filename: "{app}\picassow.exe"; Parameters: "render"; IconFilename: "{app}\picasso\gui\icons\render.ico" -Name: "{autodesktop}\Average"; Filename: "{app}\picassow.exe"; Parameters: "average"; IconFilename: "{app}\picasso\gui\icons\average.ico" -Name: "{autodesktop}\SPINNA"; Filename: "{app}\picassow.exe"; Parameters: "spinna"; IconFilename: "{app}\picasso\gui\icons\spinna.ico" -Name: "{autodesktop}\Server"; Filename: "{app}\picasso.exe"; Parameters: "server"; IconFilename: "{app}\picasso\gui\icons\server.ico" +Name: "{group}\Design"; Filename: "{app}\picassow.exe"; Parameters: "design"; IconFilename: "{app}\_internal\picasso\gui\icons\design.ico"; Components: design +Name: "{group}\Simulate"; Filename: "{app}\picassow.exe"; Parameters: "simulate"; IconFilename: "{app}\_internal\picasso\gui\icons\simulate.ico"; Components: simulate +Name: "{group}\Localize"; Filename: "{app}\picassow.exe"; Parameters: "localize"; IconFilename: "{app}\_internal\picasso\gui\icons\localize.ico"; Components: localize +Name: "{group}\Filter"; Filename: "{app}\picassow.exe"; Parameters: "filter"; IconFilename: "{app}\_internal\picasso\gui\icons\filter.ico"; Components: filter +Name: "{group}\Render"; Filename: "{app}\picassow.exe"; Parameters: "render"; IconFilename: "{app}\_internal\picasso\gui\icons\render.ico"; Components: render +Name: "{group}\Average"; Filename: "{app}\picassow.exe"; Parameters: "average"; IconFilename: "{app}\_internal\picasso\gui\icons\average.ico"; Components: average +Name: "{group}\SPINNA"; Filename: "{app}\picassow.exe"; Parameters: "spinna"; IconFilename: "{app}\_internal\picasso\gui\icons\spinna.ico"; Components: spinna +Name: "{group}\Server"; Filename: "{app}\picasso.exe"; Parameters: "server"; IconFilename: "{app}\_internal\picasso\gui\icons\server.ico"; Components: server +Name: "{group}\Nanotron"; Filename: "{app}\picassow.exe"; Parameters: "nanotron"; IconFilename: "{app}\_internal\picasso\gui\icons\nanotron.ico"; Components: nanotron +Name: "{group}\ToRaw"; Filename: "{app}\picassow.exe"; Parameters: "toraw"; IconFilename: "{app}\_internal\picasso\gui\icons\toraw.ico"; Components: toraw + + +Name: "{autodesktop}\Design"; Filename: "{app}\picassow.exe"; Parameters: "design"; IconFilename: "{app}\_internal\picasso\gui\icons\design.ico"; Components: design +Name: "{autodesktop}\Simulate"; Filename: "{app}\picassow.exe"; Parameters: "simulate"; IconFilename: "{app}\_internal\picasso\gui\icons\simulate.ico"; Components: simulate +Name: "{autodesktop}\Localize"; Filename: "{app}\picassow.exe"; Parameters: "localize"; IconFilename: "{app}\_internal\picasso\gui\icons\localize.ico"; Components: localize +Name: "{autodesktop}\Filter"; Filename: "{app}\picassow.exe"; Parameters: "filter"; IconFilename: "{app}\_internal\picasso\gui\icons\filter.ico"; Components: filter +Name: "{autodesktop}\Render"; Filename: "{app}\picassow.exe"; Parameters: "render"; IconFilename: "{app}\_internal\picasso\gui\icons\render.ico"; Components: render +Name: "{autodesktop}\Average"; Filename: "{app}\picassow.exe"; Parameters: "average"; IconFilename: "{app}\_internal\picasso\gui\icons\average.ico"; Components: average +Name: "{autodesktop}\SPINNA"; Filename: "{app}\picassow.exe"; Parameters: "spinna"; IconFilename: "{app}\_internal\picasso\gui\icons\spinna.ico"; Components: spinna +Name: "{autodesktop}\Server"; Filename: "{app}\picasso.exe"; Parameters: "server"; IconFilename: "{app}\_internal\picasso\gui\icons\server.ico"; Components: server +Name: "{autodesktop}\Nanotron"; Filename: "{app}\picassow.exe"; Parameters: "nanotron"; IconFilename: "{app}\_internal\picasso\gui\icons\nanotron.ico"; Components: nanotron +Name: "{autodesktop}\ToRaw"; Filename: "{app}\picassow.exe"; Parameters: "toraw"; IconFilename: "{app}\_internal\picasso\gui\icons\toraw.ico"; Components: toraw [Registry] Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \ diff --git a/release/one_click_windows_gui/readme.rst b/release/one_click_windows_gui/readme.rst new file mode 100644 index 00000000..74b214a8 --- /dev/null +++ b/release/one_click_windows_gui/readme.rst @@ -0,0 +1,77 @@ +One-click installer for Windows +=============================== + +This is the one-click installer for Picasso on Windows. The Picasso software is complemented by our `Nature Protocols publication `__. + +A comprehensive documentation can be found here: `Read the Docs `__. + +How to install +-------------- + +1. Download the latest release from the `release page `__. +2. Open the downloaded exe file and follow the installation instructions. + +Creating your own installer +--------------------------- + +You can create the exe file yourself by cloning our GitHub repo and running the script ``picasso/release/one_click_windows_gui/create_installer_windows.bat`` from the Command Prompt. Note that you must have conda installed on your computer. + +Adding camera configuration and plugins +--------------------------------------- + +Camera configuration is essential for correct photon conversion and thus correct localization precision calculation. For more details, see `documentation `__. + +To add your config.yaml file, navigate to your Picasso folder (by default ``C:/Picasso``) and find the subdirectory ``_internal/picasso``. Add the config file there. + +Similarly, you can add Picasso plugins under the folder ``_internal/picasso/gui/plugins``. For more details on how to create plugins, see `documentation `__. + +Changelog +--------- +To see all changes introduced across releases, see `here `_. + +Contributions & Copyright +------------------------- + +| Contributors: Joerg Schnitzbauer, Maximilian Strauss, Rafal Kowalewski, Adrian Przybylski, Andrey Aristov, Hiroshi Sasaki, Alexander Auer, Johanna Rahm +| Copyright (c) 2015-2025 Jungmann Lab, Max Planck Institute of Biochemistry +| Copyright (c) 2020-2021 Maximilian Strauss + +Citing Picasso +-------------- + +If you use Picasso in your research, please cite our Nature Protocols publication describing the software. + +| J. Schnitzbauer*, M.T. Strauss*, T. Schlichthaerle, F. Schueder, R. Jungmann +| Super-Resolution Microscopy with DNA-PAINT +| Nature Protocols (2017). 12: 1198-1228 DOI: `10.1038/nprot.2017.024 `__ +| +| If you use some of the functionalities provided by Picasso, please also cite the respective publications: + +- NeNA. DOI: `10.1007/s00418-014-1192-3 `__ +- FRC. DOI: `10.1038/nmeth.2448 `__ +- Theoretical lateral localization precision (Gauss LQ and MLE). DOI: `10.1038/nmeth.1447 `__ +- Theoretical axial localization precision (Gauss LQ and MLE). DOI: *DOI will be added once available* +- MLE fitting. DOI: `10.1038/nmeth.1449 `__ +- RCC undrifting: DOI: `10.1364/OE.22.015982 `__ +- AIM undrifting. DOI: `10.1126/sciadv.adm776 `__ +- SMLM clusterer. DOIs: `10.1038/s41467-021-22606-1 `__ and `10.1038/s41586-023-05925-9 `__ +- DBSCAN: Ester, et al. Inkdd, 1996. (Vol. 96, No. 34, pp. 226-231). +- HDBSCAN. DOI: `10.1007/978-3-642-37456-2_14 `__ +- RESI. DOI: `10.1038/s41586-023-05925-9 `__ +- Nanotron. DOI: `10.1093/bioinformatics/btaa154 `__ +- Picasso: Server. DOI: `10.1038/s42003-022-03909-5 `__ +- SPINNA. DOI: `10.1038/s41467-025-59500-z `__ +- SPINNA for LE fitting. DOI: `10.1038/s41592-024-02242-5 `__ +- G5M. DOI: *DOI will be added once available* + +Credits +------- + +- Design icon based on “Hexagon by Creative Stalls" from the Noun Project +- Simulate icon based on “Microchip by Futishia" from the Noun Project +- Localize icon based on “Mountains" by MONTANA RUCOBO from the Noun Project +- Filter icon based on “Funnel" by José Campos from the Noun Project +- Render icon based on “Paint Palette" by Vectors Market from the Noun Project +- Average icon based on “Layers" by Creative Stall from the Noun Project +- Server icon based on “Database" by Nimal Raj from the Noun Project +- SPINNA icon based on "Spinner" by Viktor Ostrovsky from the Noun Project diff --git a/release/pyinstaller/picasso.spec b/release/pyinstaller/picasso.spec index b51c3700..0a4cdb1e 100644 --- a/release/pyinstaller/picasso.spec +++ b/release/pyinstaller/picasso.spec @@ -12,6 +12,7 @@ import picasso ##################### User definitions exe_name = 'picasso' +version = '0.9.7' script_name = 'picasso_pyinstaller.py' if sys.platform[:6] == "darwin": icon = '../logos/localize.icns' @@ -107,46 +108,79 @@ pyz = PYZ( a.zipped_data, cipher=block_cipher ) - if sys.platform[:5] == "linux": - exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - name=bundle_name, - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=True, - upx_exclude=[], - icon=icon - ) -else: - exe = EXE( - pyz, - a.scripts, - # a.binaries, - a.zipfiles, - a.datas, - exclude_binaries=True, - name=exe_name, - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=True, - icon=icon - ) - coll = COLLECT( - exe, - a.binaries, - # a.zipfiles, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name=exe_name - ) \ No newline at end of file + exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name=bundle_name, + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + upx_exclude=[], + icon=icon + ) +elif sys.platform[:6] == "darwin": # macOS + exe = EXE( + pyz, + a.scripts, + a.zipfiles, + a.datas, + exclude_binaries=True, + name=exe_name, + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + icon=icon + ) + coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name=exe_name + ) + app = BUNDLE( + coll, + name=exe_name.title() + '.app', + icon=icon, + bundle_identifier='org.jungmannlab.picasso', + info_plist={ + 'NSPrincipalClass': 'NSApplication', + 'NSHighResolutionCapable': 'True', + 'CFBundleShortVersionString': version, + 'CFBundleVersion': version, + }, + ) +else: # Windows + exe = EXE( + pyz, + a.scripts, + a.zipfiles, + a.datas, + exclude_binaries=True, + name=exe_name, + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + icon=icon + ) + coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name=exe_name + ) \ No newline at end of file diff --git a/release/pyinstaller/picassow.spec b/release/pyinstaller/picassow.spec index e767f2b1..503f403c 100644 --- a/release/pyinstaller/picassow.spec +++ b/release/pyinstaller/picassow.spec @@ -12,6 +12,7 @@ import picasso ##################### User definitions exe_name = 'picasso' +version = '0.9.7' script_name = 'picasso_pyinstaller.py' if sys.platform[:6] == "darwin": icon = '../logos/localize.icns' @@ -109,44 +110,78 @@ pyz = PYZ( ) if sys.platform[:5] == "linux": - exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - name=bundle_name, - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=False, - upx_exclude=[], - icon=icon - ) -else: - exe = EXE( - pyz, - a.scripts, - # a.binaries, - a.zipfiles, - a.datas, - exclude_binaries=True, - name='picassow', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=False, - icon=icon - ) - coll = COLLECT( - exe, - a.binaries, - # a.zipfiles, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='picassow' - ) \ No newline at end of file + exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name=bundle_name, + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + upx_exclude=[], + icon=icon + ) +elif sys.platform[:6] == "darwin": # macOS + exe = EXE( + pyz, + a.scripts, + a.zipfiles, + a.datas, + exclude_binaries=True, + name=exe_name, + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + icon=icon + ) + coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name=exe_name + ) + app = BUNDLE( + coll, + name=exe_name.title() + '.app', + icon=icon, + bundle_identifier='org.jungmannlab.picasso', + info_plist={ + 'NSPrincipalClass': 'NSApplication', + 'NSHighResolutionCapable': 'True', + 'CFBundleShortVersionString': version, + 'CFBundleVersion': version, + }, + ) +else: # Windows + exe = EXE( + pyz, + a.scripts, + a.zipfiles, + a.datas, + exclude_binaries=True, + name='picassow', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + icon=icon + ) + coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='picassow' + ) \ No newline at end of file diff --git a/tests/test_clusterer.py b/tests/test_clusterer.py index 652d303d..a47044a6 100644 --- a/tests/test_clusterer.py +++ b/tests/test_clusterer.py @@ -82,6 +82,7 @@ def test_ga(locs): min_locs=GA_MIN_LOCS, frame_analysis=True, ) + assert len(clustered_locs), "No localizations returned after GA clustering" assert "group" in clustered_locs.columns, "GA did not add 'group' column" assert ( -1 not in clustered_locs["group"].to_numpy() @@ -100,6 +101,7 @@ def test_ga_3d(locs): radius_z=GA_RADIUS_Z, pixelsize=CAMERA_PIXEL_SIZE, ) + assert len(clustered_locs), "No localizations returned after GA clustering" assert "group" in clustered_locs.columns, "GA did not add 'group' column" assert ( -1 not in clustered_locs["group"].to_numpy()