diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 3dd5de58..3b4a9ef7 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.5.7 +current_version = 0.6.0 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? @@ -13,6 +13,8 @@ serialize = [bumpversion:file:./distribution/picasso.iss] +[bumpversion:file:./picasso/__init__.py] + [bumpversion:file:./picasso/__version__.py] [bumpversion:file:./release/one_click_windows_gui/picasso_innoinstaller.iss] diff --git a/changelog.rst b/changelog.rst index c877d478..3093fc3a 100644 --- a/changelog.rst +++ b/changelog.rst @@ -1,7 +1,13 @@ Changelog ========= -Last change: 07-FEB-2023 MTS +Last change: 16-FEB-2023 MTS + +0.6.0 +----- +- New RESI (Resolution Enhancement by Sequential Imaging) dialog in Picasso Render allowing for a substantial resolution boost, (*to be published*) +- Remove quantum efficiency when converting raw data into photons in Picasso Localize +- Input ROI using command-line ``picasso localize``, see `here `_. 0.5.7 ----- diff --git a/distribution/picasso.iss b/distribution/picasso.iss index d9b60b4d..088a1545 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.5.7 +AppVersion=0.6.0 DefaultDirName={pf}\Picasso DefaultGroupName=Picasso -OutputBaseFilename="Picasso-Windows-64bit-0.5.7" +OutputBaseFilename="Picasso-Windows-64bit-0.6.0" ArchitecturesAllowed=x64 ArchitecturesInstallIn64BitMode=x64 diff --git a/docs/cmd.rst b/docs/cmd.rst index 11bbc5bb..4303efe5 100644 --- a/docs/cmd.rst +++ b/docs/cmd.rst @@ -27,6 +27,7 @@ The reconstruction parameters can be specified by adding respective arguments. I ‘-a’, ‘–fit-method’, choices=["mle", "lq", "lq-gpu", "lq-3d", "lq-gpu-3d", "avg"], default=‘mle’ ‘-g’, ‘–gradient’, type=int, default=5000, help=‘minimum net gradient’ ‘-d’, ‘–drift’, type=int, default=1000, help=‘segmentation size for subsequent RCC, 0 to deactivate’ + ‘-r’, ‘-roi‘, type=int, nargs=4, default=None, help=‘ROI (y_min, x_min, y_max, x_max) in camera pixels’ ‘-bl’, ‘–baseline’, type=int, default=0, help=‘camera baseline’ ‘-s’, ‘–sensitivity’, type=int, default=1, help=‘camera sensitivity’ ‘-ga’, ‘–gain’, type=int, default=1, help=‘camera gain’ diff --git a/docs/conf.py b/docs/conf.py index 49e6c504..ab598341 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.5.7" +release = "0.6.0" # -- General configuration --------------------------------------------------- diff --git a/docs/localize.rst b/docs/localize.rst index cffbef48..320e3b3c 100644 --- a/docs/localize.rst +++ b/docs/localize.rst @@ -9,7 +9,9 @@ Localize allows performing super-resolution reconstruction of image stacks. For - MLE, integrated Gaussian (based on `Smith et al., 2014 `_.) - LQ, Gaussian (least squares) -- Average of ROI +- Average of ROI (finds summed intensity of spots) + +**Please note:** Picasso Localize supports five file formats: ``.ome.tif``, ``NDTiffStack`` with extension ``.tif``, ``.raw``, ``.ims`` and ``.nd2``. If your file has the extension ``.tiff`` or ``.ome.tiff``, it cannot be read. Usually it is enough to change the extension to ``.ome.tif``, i.e., remove the last letter. Identification and fitting of single-molecule spots --------------------------------------------------- diff --git a/docs/render.rst b/docs/render.rst index 746f4fde..ffcf1270 100644 --- a/docs/render.rst +++ b/docs/render.rst @@ -26,7 +26,7 @@ Redundant cross-correlation drift correction Marker-based drift correction ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -1. In ``Picasso: Render``, pick drift markers as described in ``Picking of regions of interest``. Use the ``Pick similar`` option to automatically detect a large number of drift markers similar to a few manually selected ones. +1. In ``Picasso: Render``, pick drift markers as described in **Picking of regions of interest**. Use the ``Pick similar`` option to automatically detect a large number of drift markers similar to a few manually selected ones. 2. If the structures used as drift markers have an intrinsic size larger than the precision of individual localizations (e.g., DNA origami, large protein complexes), it is critical to select a large number of structures. Otherwise, the statistic for calculating the drift in each frame (the mean displacement of localization to the structure's center of mass) is not valid. 3. Select ``Postprocess > Undrift from picked`` to compute and apply the drift correction. 4. (Optional) Save the drift-corrected localizations by selecting ``File > Save localizations``. @@ -54,6 +54,21 @@ Rotation around z-axis is available by pressing Ctrl/Command. Rotation axis can There are several things to keep in mind when using the rotation window. Firstly, using individual localization precision is very slow and is not recommended as a default blur method. Also, the size of the rotation window can be altered, however, if it becomes too large, rendering may start to lag. +RESI +---- +.. image:: ../docs/render_resi.png + :width: 374 + :alt: UML Render RESI + + +In Picasso 0.6.0, a new RESI (Resolution Enhancement by Sequential Imaging) dialog was introduced. It allows for a substantial resolution boost by sequential imaging of a single target with multiple labels with Exchange-PAINT (*to be published*). + +To use RESI, prepare your individual RESI channels (localization, undrifting, filtering and **alignment**). Load such localization lists into Picasso Render and open ``Postprocess > RESI``. The dialog shown above will appear. Each channel will be clustered using the SMLM clusterer (other clustering algorithms could be applied as well although only the SMLM clusterer is implemented for RESI in Picasso). Clustering parameters can be defined for each RESI channel individually, although it is possible to apply the same parameters to all channels by clicking ``Apply the same clustering parameters to all channels``, which will copy the clustering parameters from the first row and paste it to all other channels. + +Next, the user needs to specify whether or not to save clustered localizations or cluster centers from each of the RESI channels individually, and whether to apply basic frame analysis (to minimize the effect of sticking events). For the explanation of the parameters, see `SMLM clusterer `_. + +Upon clicking ``Perform RESI analysis``, each of the loaded channels is clustered, cluster centers are extracted and combined from all RESI channels to create the final RESI file. + Dialogs ------- @@ -378,7 +393,11 @@ Apply expressions to localizations This tool allows you to apply expressions to localizations, for example: - ``x +=1`` will shift all localization by one to the right -- ``x +=1;y+=1`` will shift all localization by one to the right and one up. +- ``x +=1; y+=1`` will shift all localization by one to the right and one up. +- ``flip x z`` will exchange the x-axis with y-axis if z localizations are present (side projection), similar for ``flip y z``. +- ``spiral r n`` will plot each localization over the time of the movie in a spiral with radius r and n number of turns (e.g., to detect repetitive binding), ``uspiral`` to reverse. + +**NOTE:** using two variables in one statement is not supported (e.g. ``x = y``) To filter localizations use picasso filter. DBSCAN ^^^^^^ @@ -390,9 +409,16 @@ Cluster localizations with the hdbscan clustering algorithm. SMLM clusterer ^^^^^^^^^^^^^^ -Cluster localizations with the custom algorithm designed for SMLM. In short, localizations with the maximum number of neighboring localizations within a user-defined radius are chosen as cluster centers, around which all localizations withing the given radius belong to one cluster. If two or more such clusters overlap, they are combined. +Cluster localizations with the custom algorithm designed for SMLM. In short, localizations with the maximum number of neighboring localizations within a user-defined radius are chosen as cluster centers, around which all localizations within the given radius belong to one cluster. If two or more local maxima are within the radius, the clusters are merged. + +SMLM clusterer requires three (or four if 3D data is processed) arguments: + +- Radius: final size of the clusters. +- Radius z (3D only): final size of the clusters in the z axis. If the value is different from radius in xy plane, clusters have ellipsoidal shape. Radius z can have a different value to account for a difference in localization precision in lateral and axial directions. +- Min. locs: minimum number of localizations in a cluster. +- Basic frame analysis: If True, each cluster is checked for its value of mean frame (if it is within the first or the last 20% of the total acquisition time, it is discarded). Moreover, localizations inside each cluster are split into 20 time bins (across the whole acquisition time). If a single time bin contains more than 80% of localizations per cluster, the cluster is discarded. -*NOTE:* it is highly recommended to remove any fiducial markers before clustering, to lower clustering time, given they are of no interest to the user. To do that, the markers can be picked and removed using ``Tools > Remove localizations in picks``. +**Note to all clustering algorithms:** it is highly recommended to remove any fiducial markers before clustering, to lower clustering time, given they are of no interest to the user. To do that, the markers can be picked and removed using ``Tools > Remove localizations in picks``. Test clusterer ^^^^^^^^^^^^^^ @@ -400,13 +426,4 @@ Opens a dialog where different clustering parameters can be checked on the loade Nearest Neighbor Analysis ^^^^^^^^^^^^^^^^^^^^^^^^^ -Calculates distances to the ``k``-th nearest neighbors between two channels (can be the same channel). ``k`` is defined by the user. The distances are stored in nm as a .csv file. - -Notes -+++++ -Using two variables in one statement is not supported (e.g. ``x = y``) To filter localizations use picasso filter. - -Additional commands -+++++++++++++++++++ -``flip x z`` will exchange the x-axis with y-axis if z localizations are present (side projection), similar for ``flip y z``. -``spiral r n`` will plot each localization over the time of the movie in a spiral with radius r and n number of turns (e.g., to detect repetitive binding), ``uspiral`` to reverse. +Calculates distances to the ``k``-th nearest neighbors between two channels (can be the same channel). ``k`` is defined by the user. The distances are stored in nm as a .csv file. \ No newline at end of file diff --git a/docs/render_resi.png b/docs/render_resi.png new file mode 100644 index 00000000..3a7a188d Binary files /dev/null and b/docs/render_resi.png differ diff --git a/picasso/__init__.py b/picasso/__init__.py index e3c2b26f..bc17dd18 100644 --- a/picasso/__init__.py +++ b/picasso/__init__.py @@ -2,12 +2,13 @@ picasso/__init__.py ~~~~~~~~~~~~~~~~~~~~ - :authors: Joerg Schnitzbauer, Maximilian Thomas Strauss, 2016-2018 + :authors: Joerg Schnitzbauer, Maximilian Thomas Strauss, Rafal Kowalewski 2016-2023 :copyright: Copyright (c) 2016-2018 Jungmann Lab, MPI of Biochemistry """ import os.path as _ospath import yaml as _yaml +__version__ = "0.6.0" _this_file = _ospath.abspath(__file__) _this_dir = _ospath.dirname(_this_file) @@ -17,4 +18,4 @@ if CONFIG is None: CONFIG = {} except FileNotFoundError: - CONFIG = {} + CONFIG = {} \ No newline at end of file diff --git a/picasso/__main__.py b/picasso/__main__.py index b63c7460..7052b001 100644 --- a/picasso/__main__.py +++ b/picasso/__main__.py @@ -760,7 +760,6 @@ def _localize(args): locs_from_fits, add_file_to_db, ) - from os.path import splitext, isdir from time import sleep from . import gausslq, avgroi @@ -780,7 +779,10 @@ def _localize(args): raise Exception("GPUfit not installed. Aborting.") for index, element in enumerate(vars(args)): - print("{:<8} {:<15} {:<10}".format(index + 1, element, getattr(args, element))) + try: + print("{:<8} {:<15} {:<10}".format(index + 1, element, getattr(args, element))) + except TypeError: # if None is default value + print("{:<8} {:<15} {}".format(index + 1, element, "None")) print("------------------------------------------") def check_consecutive_tif(filepath): @@ -849,8 +851,13 @@ def prompt_info(): save_info(info_path, [info]) if paths: + print(args) box = args.box_side_length min_net_gradient = args.gradient + roi = args.roi + if roi is not None: + y_min, x_min, y_max, x_max = roi + roi = [[y_min, x_min], [y_max, x_max]] camera_info = {} camera_info["baseline"] = args.baseline camera_info["sensitivity"] = args.sensitivity @@ -898,7 +905,7 @@ def prompt_info(): print("Processing {}, File {} of {}".format(path, i + 1, len(paths))) print("------------------------------------------") movie, info = load_movie(path) - current, futures = identify_async(movie, min_net_gradient, box) + current, futures = identify_async(movie, min_net_gradient, box, roi=roi) n_frames = len(movie) while current[0] < n_frames: print( @@ -1435,6 +1442,17 @@ def main(): default=1000, help="segmentation size for subsequent RCC, 0 to deactivate", ) + localize_parser.add_argument( + "-r", + "--roi", + type=int, + nargs=4, + default=None, + help=( + "ROI (y_min, x_min, y_max, x_max) in camera pixels;\n" + "note the origin of the image is in the top left corner" + ), + ) localize_parser.add_argument( "-bl", "--baseline", type=int, default=0, help="camera baseline" ) @@ -1468,7 +1486,7 @@ def main(): default="", help="Suffix to add to files", ) - + localize_parser.add_argument( "-db", "--database", diff --git a/picasso/__version__.py b/picasso/__version__.py index ebc7d661..970bd0e1 100644 --- a/picasso/__version__.py +++ b/picasso/__version__.py @@ -1 +1 @@ -VERSION_NO = "0.5.7" +VERSION_NO = "0.6.0" diff --git a/picasso/clusterer.py b/picasso/clusterer.py index 55aaf4f6..c5563a97 100644 --- a/picasso/clusterer.py +++ b/picasso/clusterer.py @@ -18,10 +18,7 @@ from . import lib as _lib -# from icecream import ic - CLUSTER_CENTERS_DTYPE_2D = [ - ("group", "i4"), ("frame", "f4"), ("std_frame", "f4"), ("x", "f4"), @@ -39,9 +36,9 @@ ("n", "u4"), ("area", "f4"), ("convexhull", "f4"), + ("group", "i4"), ] CLUSTER_CENTERS_DTYPE_3D = [ - ("group", "i4"), ("frame", "f4"), ("std_frame", "f4"), ("x", "f4"), @@ -61,7 +58,8 @@ ("n", "u4"), ("volume", "f4"), ("convexhull", "f4"), -] # for saving cluster centers + ("group", "i4"), +] def _frame_analysis(frame, n_frames): @@ -110,6 +108,7 @@ def _frame_analysis(frame, n_frames): return passed + def frame_analysis(labels, frame): """ Performs basic frame analysis on clustered localizations. @@ -148,6 +147,7 @@ def frame_analysis(labels, frame): return labels + def _cluster(X, radius, min_locs, frame=None): """ Clusters points given by X with a given clustering radius and @@ -208,11 +208,19 @@ def _cluster(X, radius, min_locs, frame=None): if len(idx): # if such a loc exists, assign it to a cluster labels[idx] = label + ## check for number of locs per cluster to be above min_locs + values, counts = _np.unique(labels, return_counts=True) + # labels to discard if has fewer locs than min_locs + to_discard = values[counts < min_locs] + # substitute this with -1 + labels[_np.isin(labels, to_discard)] = -1 + if frame is not None: labels = frame_analysis(labels, frame) return labels + def cluster_2D(x, y, frame, radius, min_locs, fa): """ Prepares 2D input to be used by _cluster() @@ -248,6 +256,7 @@ def cluster_2D(x, y, frame, radius, min_locs, fa): return labels + def cluster_3D(x, y, z, frame, radius_xy, radius_z, min_locs, fa): """ Prepares 3D input to be used by _cluster() @@ -290,6 +299,7 @@ def cluster_3D(x, y, z, frame, radius_xy, radius_z, min_locs, fa): return labels + def cluster(locs, params, pixelsize): """ Clusters localizations given user parameters using KDTree. @@ -337,6 +347,7 @@ def cluster(locs, params, pixelsize): locs = extract_valid_labels(locs, labels) return locs + def _dbscan(X, radius, min_density): """ Finds DBSCAN cluster labels, given data points and parameters. @@ -362,6 +373,7 @@ def _dbscan(X, radius, min_density): db = _DBSCAN(eps=radius, min_samples=min_density).fit(X) return db.labels_.astype(_np.int32) + def dbscan(locs, radius, min_density, pixelsize): """ Performs DBSCAN on localizations. @@ -393,6 +405,7 @@ def dbscan(locs, radius, min_density, pixelsize): locs = extract_valid_labels(locs, labels) return locs + def _hdbscan(X, min_cluster_size, min_samples, cluster_eps=0): """ Finds HDBSCAN cluster labels, given data points and parameters. @@ -425,6 +438,7 @@ def _hdbscan(X, min_cluster_size, min_samples, cluster_eps=0): ).fit(X) return hdb.labels_.astype(_np.int32) + def hdbscan(locs, min_cluster_size, min_samples, pixelsize, cluster_eps=0.): """ Performs HDBSCAN on localizations. @@ -460,6 +474,7 @@ def hdbscan(locs, min_cluster_size, min_samples, pixelsize, cluster_eps=0.): locs = extract_valid_labels(locs, labels) return locs + def extract_valid_labels(locs, labels): """ Extracts localizations based on clustering results. @@ -488,6 +503,7 @@ def extract_valid_labels(locs, labels): locs = locs[locs.group != -1] return locs + def error_sums_wtd(x, w): """ Function used for finding localization precision for cluster @@ -508,6 +524,7 @@ def error_sums_wtd(x, w): return (w * (x - (w * x).sum() / w.sum())**2).sum() / w.sum() + def find_cluster_centers(locs, pixelsize): """ Calculates cluster centers. @@ -559,7 +576,6 @@ def find_cluster_centers(locs, pixelsize): convexhull = _np.array([_[18] for _ in centers_]) centers = _np.rec.array( ( - res.index.values, # group id frame, std_frame, x, @@ -579,6 +595,7 @@ def find_cluster_centers(locs, pixelsize): n, volume, convexhull, + res.index.values, # group id ), dtype=CLUSTER_CENTERS_DTYPE_3D, ) @@ -587,7 +604,6 @@ def find_cluster_centers(locs, pixelsize): convexhull = _np.array([_[14] for _ in centers_]) centers = _np.rec.array( ( - res.index.values, # group id frame, std_frame, x, @@ -605,6 +621,7 @@ def find_cluster_centers(locs, pixelsize): n, area, convexhull, + res.index.values, # group id ), dtype=CLUSTER_CENTERS_DTYPE_2D, ) @@ -615,6 +632,7 @@ def find_cluster_centers(locs, pixelsize): return centers + def cluster_center(grouplocs, pixelsize, separate_lp=False): """ Finds cluster centers and their attributes. diff --git a/picasso/gui/localize.py b/picasso/gui/localize.py index a68ec091..f6178a09 100644 --- a/picasso/gui/localize.py +++ b/picasso/gui/localize.py @@ -18,7 +18,6 @@ import traceback import importlib, pkgutil from .. import io, localize, gausslq, gaussmle, zfit, lib, CONFIG, avgroi -# from icecream import ic from collections import UserDict try: @@ -431,7 +430,7 @@ def __init__(self, parent=None): identification_grid.addWidget(self.box_spinbox, 0, 1) # Min. Net Gradient - identification_grid.addWidget(QtWidgets.QLabel("Min. Net Gradient:"), 1, 0) + identification_grid.addWidget(QtWidgets.QLabel("Min. Net Gradient:"), 1, 0) self.mng_spinbox = QtWidgets.QSpinBox() self.mng_spinbox.setRange(0, 1e9) self.mng_spinbox.setValue(DEFAULT_PARAMETERS["Min. Net Gradient"]) @@ -470,11 +469,26 @@ def __init__(self, parent=None): self.mng_max_spinbox.valueChanged.connect(self.on_mng_max_changed) hbox.addWidget(self.mng_max_spinbox) + # # ROI + # identification_grid.addWidget( + # QtWidgets.QLabel("ROI (y_min,x_min,y_max,x_max):"), 4, 0, + # ) + # self.roi_edit = QtWidgets.QLineEdit() + # regex = r"\d+,\d+,\d+,\d+" # regex for 4 integers separated by commas + # validator = QtGui.QRegExpValidator(QtCore.QRegExp(regex)) + # self.roi_edit.setValidator(validator) + # self.roi_edit.editingFinished.connect(self.on_roi_edit_finished) + # identification_grid.addWidget(self.roi_edit, 4, 1) + # #TODO: signal when roi_edit is changed: change self.roi and draw rectangle? + # #TODO: validate that the input numbers lie within the whole FOV + # #TODO: when roi is changed with mouseReleaseEvent in View, change the values displayed here! + # #TODO: what about nan? + self.preview_checkbox = QtWidgets.QCheckBox("Preview") self.preview_checkbox.setTristate(False) - # self.preview_checkbox.setChecked(True) self.preview_checkbox.stateChanged.connect(self.on_preview_changed) identification_grid.addWidget(self.preview_checkbox, 4, 0) + # identification_grid.addWidget(self.preview_checkbox, 5, 0) # Camera: if "Cameras" in CONFIG: @@ -759,6 +773,22 @@ def reset_quality_check(self): for idx, _ in enumerate(self.quality_grid_values): _.setVisible(False) _.setText("") + + # def on_roi_edit_finished(self): + # from icecream import ic # TODO:delete + # text = self.roi_edit.text().split(",") + # y_min, x_min, y_max, x_max = [int(_) for _ in text] + # # update roi + # self.window.view.roi = [[y_min, x_min], [y_max, x_max]] + # # draw rectangle TODO use self.window.view.rubberband + # self.window.view.rubberband.setGeometry( + # QtCore.QRect(x_min, y_min, x_max-x_min, y_max-y_min) + # ) + # self.window.view.rubberband.show() + # #TOOD: incorrect indeces, (wrong place for the box,) + # #TODO: box dispaperast afte rcllicking on View + # #TODO: use map to scene??, idk + # self.window.draw_frame() def on_fit_method_changed(self, state): if self.fit_method.currentText() == "LQ, Gaussian": diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 339ac869..e1ebb5aa 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -87,7 +87,7 @@ def get_colors(n_channels): colors = [colorsys.hsv_to_rgb(_, 1, 1) for _ in hues] return colors -def get_render_properties_colors(n_channels): +def get_render_properties_colors(n_channels, cmap='gist_rainbow'): """ Creates a list with rgb channels for each of the channels used in rendering property using the gist_rainbow colormap, see: @@ -96,16 +96,18 @@ def get_render_properties_colors(n_channels): Parameters ---------- n_channels : int - Number of locs channels + Number of locs channels. + cmap : str (default='gist_rainbow') + Colormap name. Returns ------- - list - Contains tuples with rgb channels + colors : list of tuples + Contains tuples with rgb channels. """ # array of shape (256, 3) with rbh channels with 256 colors - base = plt.get_cmap('gist_rainbow')(np.arange(256))[:, :3] + base = plt.get_cmap(cmap)(np.arange(256))[:, :3] # indeces to draw from base idx = np.linspace(0, 255, n_channels).astype(int) # extract the colors of interest @@ -708,9 +710,9 @@ def add_entry(self, path): if self.warning: text = ( "The number of channels passed the number of default " - " colors. In case you would like to use your own color, " + " colors. In case you would like to use your own color, " " please insert the color's hexadecimal expression," - " starting with '#', e.g. '#ffcdff' for pink or choose" + " starting with '#', e.g. '#ffcdff' for pink or choose" " the automatic coloring in the Files dialog." ) QtWidgets.QMessageBox.information(self, "Warning", text) @@ -3928,6 +3930,336 @@ def __init__(self, window, tools_settings_dialog): self.grid.setRowStretch(1, 1) +class RESIDialog(QtWidgets.QDialog): + """ RESI dialog. + + Allows for clustering multiple channels with user-defined + clustering parameters using the SMLM clusterer; saves cluster + centers in a single .hdf5 file that contains an extra column with + resi channel ids and a metadata .yaml file. + + ... + + Attributes + ---------- + apply_fa : QCheckBox + If checked, apply basic frame analysis (just like in the case + of SMLM clustering) + locs : list of np.recarrays + List of localization lists that are loaded in the main window + when opening the RESI dialog. + min_locs : list of QSpinBoxes + List of widgets holding minimum number of localizations used in + SMLM clusterer. + n_dim : int + Dimensionality of loaded localizations (2 or 3). + n_channels : int + Number of channels used in RESI analysis. + paths : list of strings + Paths to localization lists used for RESI analysis. + radius_xy : list of QDoubleSpinBoxes + List of widgets holding radius in x and y used in SMLM + clusterer. + radius_z : list of QDoubleSpinBoxes + List of widgets holding radius in z used in SMLM clusterer. + Only applied when 3D data is loaded. Otherwise is not used. + save_cluster_centers : QCheckBox + If checked, saves cluster centers for each loaded localization + list while performing RESI analysis. + save_clustered_locs : QCheckBox + If checked, saves clustered localizations for each loaded + localization list while performing RESI analysis. + window : QMainWindow + Instance of the main Picasso Render window. + + Methods + ------- + on_same_params_clicked() + Sets all clustering parameters to have the same value as in + the first row. + perform_resi() + Performs RESI and saves RESI cluster centers. Saves clustered + localizations and cluster centers if requested. + """ + + def __init__(self, window): + super().__init__() + self.setWindowTitle("RESI") + this_directory = os.path.dirname(os.path.realpath(__file__)) + icon_path = os.path.join(this_directory, "icons", "render.ico") + icon = QtGui.QIcon(icon_path) + self.setWindowIcon(icon) + + self.window = window + self.locs = window.view.locs + self.n_channels = len(self.locs) + self.paths = window.view.locs_paths + self.ndim = 2 + if all([hasattr(_, "z") for _ in self.locs]): + self.ndim = 3 + + self.radius_xy = [] + self.radius_z = [] + self.min_locs = [] + + ### layout ### + vbox = QtWidgets.QVBoxLayout(self) + + ## clustering parameters - apply the same to all channels + params_box = QtWidgets.QGroupBox("") + vbox.addWidget(params_box) + params_grid = QtWidgets.QGridLayout(params_box) + + same_params = QtWidgets.QPushButton( + "Apply the same clustering parameters to all channels" + ) + same_params.setAutoDefault(False) + same_params.clicked.connect(self.on_same_params_clicked) + params_grid.addWidget(same_params, 0, 0, 1, 4) + + ## clustering parameters - labels + params_grid.addWidget(QtWidgets.QLabel("RESI channel"), 2, 0) + if self.ndim == 2: + params_grid.addWidget( + QtWidgets.QLabel("Radius\n[cam. pixel]"), 2, 1 + ) + params_grid.addWidget( + QtWidgets.QLabel("Min # localizations"), 2, 2, 1, 2 + ) + else: + params_grid.addWidget( + QtWidgets.QLabel("Radius xy\n[cam. pixel]"), 2, 1 + ) + params_grid.addWidget( + QtWidgets.QLabel("Radius z\n[cam. pixel]"), 2, 2 + ) + params_grid.addWidget( + QtWidgets.QLabel("Min # localizations"), 2, 3 + ) + + ## clustering parameters - values + for i in range(self.n_channels): + channel_name = self.window.dataset_dialog.checks[i].text() + count = params_grid.rowCount() + + r_xy = QtWidgets.QDoubleSpinBox() + r_xy.setRange(0.0001, 1e3) + r_xy.setDecimals(4) + r_xy.setValue(0.1) + r_xy.setSingleStep(0.01) + self.radius_xy.append(r_xy) + + r_z = QtWidgets.QDoubleSpinBox() + r_z.setRange(0.0001, 1e3) + r_z.setDecimals(4) + r_z.setValue(0.25) + r_z.setSingleStep(0.01) + self.radius_z.append(r_z) + + min_locs = QtWidgets.QSpinBox() + min_locs.setRange(1, 1e6) + min_locs.setValue(10) + min_locs.setSingleStep(1) + self.min_locs.append(min_locs) + + params_grid.addWidget(QtWidgets.QLabel(channel_name), count, 0) + params_grid.addWidget(r_xy, count, 1) + if self.ndim == 3: + params_grid.addWidget(r_z, count, 2) + params_grid.addWidget(min_locs, count, 3) + else: + params_grid.addWidget(min_locs, count, 2, 1, 2) + + ## perform clustering + # what to save + self.save_clustered_locs = QtWidgets.QCheckBox( + "Save clustered localizations\nof individual channels" + ) + self.save_clustered_locs.setChecked(False) + params_grid.addWidget( + self.save_clustered_locs, params_grid.rowCount(), 0, 1, 2 + ) + # individual cluster centers + self.save_cluster_centers = QtWidgets.QCheckBox( + "Save cluster centers\nof individual channels" + ) + self.save_cluster_centers.setChecked(False) + params_grid.addWidget( + self.save_cluster_centers, params_grid.rowCount()-1, 2, 1, 2 + ) + # apply basic frame analysis + self.apply_fa = QtWidgets.QCheckBox( + "Apply frame analysis\nto clustered localizations" + ) + self.apply_fa.setChecked(True) + params_grid.addWidget(self.apply_fa, params_grid.rowCount(), 0, 1, 2) + + ## perform resi button + resi_button = QtWidgets.QPushButton("Perform RESI analysis") + resi_button.clicked.connect(self.perform_resi) + params_grid.addWidget(resi_button, params_grid.rowCount()-1, 2, 1, 2) + + def on_same_params_clicked(self): + """ Sets all clustering parameters to have the same value as in + the first row. + """ + + for r_xy, r_z, m in zip(self.radius_xy, self.radius_z, self.min_locs): + r_xy.setValue(self.radius_xy[0].value()) + r_z.setValue(self.radius_z[0].value()) + m.setValue(self.min_locs[0].value()) + + def perform_resi(self): + """ Performs RESI analysis on loaded localizations, using + user-defined clustering parameters. + """ + + ### Sanity check if more than one channel is present + if self.n_channels < 2: + message = ( + "RESI relies on sequential imaging to assure sufficient" + " sparsity of the binding sites. Thus, it requires at least" + " two localization lists to be loaded.\n" + "If you wish to extract cluster centers, please use\n" + "Postprocess > Clustering > SMLM Clusterer" + ) + QtWidgets.QMessageBox.information(self, "Warning", message) + return + + ### Prepare data + # extract clustering parameters + r_xy = [_.value() for _ in self.radius_xy] + r_z = [_.value() for _ in self.radius_z] + min_locs = [_.value() for _ in self.min_locs] + + # get camera pixel size + pixelsize = self.window.display_settings_dlg.pixelsize.value() + + # saving: path and info for the resi file, suffices for saving + # clustered localizations and cluster centers if requested + suffix_locs = None # suffix added to clustered locs + suffix_centers = None # suffix added to cluster centers + apply_fa = self.apply_fa.isChecked() # apply basic frame analysis? + + resi_path, ext = QtWidgets.QFileDialog.getSaveFileName( + self.window, + "Save RESI cluster centers", + self.paths[0].replace(".hdf5", "_resi.hdf5"), + filter="*.hdf5", + ) + info = self.window.view.infos[0] + new_info = { + "Paths to RESI channels": self.paths, + "Clustering radius xy [cam. pixels] for each channel": r_xy, + "Min. number of locs in a cluster for each channel": min_locs, + "Basic frame analysis": apply_fa, + } + if self.ndim == 3: + new_info[ + "Clustering radius z [cam. pixels] for each channel" + ] = r_z + resi_info = info + [new_info] + + if resi_path: + ok1 = False + if self.save_clustered_locs.isChecked(): + suffix_locs, ok1 = QtWidgets.QInputDialog.getText( + self, + "", + "Enter suffix for saving clustered localizations", + QtWidgets.QLineEdit.Normal, + "_clustered", + ) + ok2 = False + if self.save_cluster_centers.isChecked(): + suffix_centers, ok2 = QtWidgets.QInputDialog.getText( + self, + "", + "Enter suffix for saving cluster centers", + QtWidgets.QLineEdit.Normal, + "_cluster_centers", + ) + + ### Perform RESI + progress = lib.ProgressDialog( + "Performing RESI analysis...", 0, self.n_channels, self.window + ) + progress.set_value(0) + progress.show() + + resi_channels = [] # holds each channel's cluster centers + for i, locs in enumerate(self.locs): + # cluster each channel using SMLM clusterer + if self.ndim == 3: + params = [r_xy[i], r_z[i], min_locs[i], 0, apply_fa, 0] + else: + params = [r_xy[i], min_locs[i], 0, apply_fa, 0] + + clustered_locs = clusterer.cluster(locs, params, pixelsize) + + # save clustered localizations if requested + if ok1: + new_info = { + "Clustering radius xy [cam. pixels]": r_xy[i], + "Min. number of locs": min_locs[i], + "Basic frame analysis": apply_fa, + } + if self.ndim == 3: + new_info["Clustering radius z [cam. pixels]"] = r_z[i] + io.save_locs( + self.paths[i].replace( + ".hdf5", f"{suffix_locs}.hdf5" + ), + clustered_locs, + self.window.view.infos[i] + [new_info], + ) + + # extract cluster centers for each channel + centers = clusterer.find_cluster_centers( + clustered_locs, pixelsize + ) + # save cluster centers if requested + if ok2: + new_info = { + "Clustering radius xy [cam. pixels]": r_xy[i], + "Min. number of locs": min_locs[i], + "Basic frame analysis": apply_fa, + } + if self.ndim == 3: + new_info["Clustering radius z [cam. pixels]"] = r_z[i] + io.save_locs( + self.paths[i].replace( + ".hdf5", f"{suffix_centers}.hdf5" + ), + centers, + self.window.view.infos[i] + [new_info], + ) + # append resi channel id + centers = lib.append_to_rec( + centers, + i*np.ones(len(centers), dtype=np.int8), + "resi_channel_id", + ) + resi_channels.append(centers) + progress.set_value(i) + progress.close() + + # combine resi cluster centers from all channels + all_resi = stack_arrays( + resi_channels, + asrecarray=True, + usemask=False, + autoconvert=True, + ) + # discard group info from resi cluster centers + all_resi = lib.remove_from_rec(all_resi, "group") + # sort like all Picasso localization lists + all_resi.sort(kind="mergesort", order="frame") + + # save resi cluster centers + io.save_locs(resi_path, all_resi, resi_info) + + class ToolsSettingsDialog(QtWidgets.QDialog): """ A dialog class to customize picks - vary shape and size, annotate, @@ -4315,7 +4647,7 @@ def on_cmap_changed(self): cmap = np.load(path) if cmap.shape != (256, 4): raise ValueError( - "Colormap must be of shape (256, 4)\n" + "Colormap must be of shape (256, 4)\n" f"The loaded colormap has shape {cmap.shape}" ) self.colormap.setCurrentText("magma") @@ -8474,9 +8806,9 @@ def read_colors(self, n_channels=None): else: warning = ( "The color selection not recognnised in the channel " - " {}. Please choose one of the options provided or " + " {}Please choose one of the options provided or " " type the hexadecimal code for your color of choice, " - " starting with '#', e.g. '#ffcdff' for pink.".format( + " starting with '#', e.g. '#ffcdff' for pink.".format( self.window.dataset_dialog.checks[i].text() ) ) @@ -9748,7 +10080,7 @@ def update_pick_info_long(self): """ Called when evaluating picks statistics in Info Dialog. """ if len(self._picks) == 0: - warning = "No picks found. Please pick first." + warning = "No picks foundPlease pick first." QtWidgets.QMessageBox.information(self, "Warning", warning) return @@ -10506,6 +10838,10 @@ def initUI(self, plugins_loaded): nn_action = postprocess_menu.addAction("Nearest Neighbor Analysis") nn_action.triggered.connect(self.view.nearest_neighbor) + postprocess_menu.addSeparator() + resi_action = postprocess_menu.addAction("RESI") + resi_action.triggered.connect(self.open_resi_dialog) + self.load_user_settings() # Define 3D entries @@ -11293,7 +11629,7 @@ def subtract_picks(self): if path: self.view.subtract_picks(path) else: - warning = "No picks found. Please pick first." + warning = "No picks found. Please pick first." QtWidgets.QMessageBox.information(self, "Warning", warning) def load_user_settings(self): @@ -11724,6 +12060,11 @@ def update_info(self): except AttributeError: pass + def open_resi_dialog(self): + resi_dialog = RESIDialog(self) + self.dialogs.append(resi_dialog) + resi_dialog.show() + def main(): app = QtWidgets.QApplication(sys.argv) diff --git a/picasso/io.py b/picasso/io.py index a68b64a8..ed1d10bf 100644 --- a/picasso/io.py +++ b/picasso/io.py @@ -191,6 +191,8 @@ def load_movie(path, prompt_info=None, progress=None): return load_ims(path, prompt_info=prompt_info) elif ext == '.nd2': return load_nd2(path) + elif ext == ".tiff": + print("Extension .tiff not supported, please use .ome.tif instead.") def load_info(path, qt_parent=None): path_base, path_extension = _ospath.splitext(path) diff --git a/picasso/localize.py b/picasso/localize.py index abaadde6..6f99b0ed 100755 --- a/picasso/localize.py +++ b/picasso/localize.py @@ -366,7 +366,8 @@ def _to_photons(spots, camera_info): sensitivity = camera_info["sensitivity"] gain = camera_info["gain"] qe = camera_info["qe"] - return (spots - baseline) * sensitivity / (gain * qe) + # since v0.5.8: remove quantum efficiency to better reflect precision + return (spots - baseline) * sensitivity / (gain) def get_spots(movie, identifications, box, camera_info): diff --git a/readme.rst b/readme.rst index 7549fda3..4af17d07 100644 --- a/readme.rst +++ b/readme.rst @@ -22,17 +22,28 @@ Picasso A collection of tools for painting super-resolution images. The Picasso software is complemented by our `Nature Protocols publication `__. A comprehensive documentation can be found here: `Read the Docs `__. + +Picasso 0.6.0 +------------- + +RESI (Resolution Enhancement by Sequential Imaging) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +RESI dialog added to Picasso Render, allowing for substatial boost in spatial resolution (*to be published*). + Photon conversion update ------------------------------ -In the next Picasso update **(0.5.8)** the formula for conversion of raw data to photons will be changed **affecting localization precision calculated** as the number of photons changes. +~~~~~~~~~~~~~~~~~~~~~~~~ +The formula for conversion of raw data to photons was changed, resulting in different numbers of photons and thus **affecting the localization precision** accordingly. Until version *0.5.7*, the formula was: -*(RAW_DATA - BASELINE) x SENSITIVITY / (GAIN x QE)*, where QE is quantum efficiency of the camera. In the new version it will be changed too: +*(RAW_DATA - BASELINE) x SENSITIVITY / (GAIN x QE)*, where QE is quantum efficiency of the camera. + +In Picasso *0.6.0* it was changed to: *(RAW_DATA - BASELINE) x SENSITIVITY / GAIN* -**i.e., quantum effiency will be removed.** +**i.e., quantum effiency was removed.** Thus, the estimate of the localization precision better approximates the true precision. + For backward compatibility, quantum efficiency will be kept in Picasso Localize, however, it will have no effect on the new photon conversion formula. diff --git a/release/one_click_windows_gui/create_installer_windows.bat b/release/one_click_windows_gui/create_installer_windows.bat index 73e9beae..0fd5e79a 100644 --- a/release/one_click_windows_gui/create_installer_windows.bat +++ b/release/one_click_windows_gui/create_installer_windows.bat @@ -11,7 +11,7 @@ call conda activate picasso_installer call python setup.py sdist bdist_wheel call cd release/one_click_windows_gui -call pip install "../../dist/picassosr-0.5.7-py3-none-any.whl" +call pip install "../../dist/picassosr-0.6.0-py3-none-any.whl" call pip install pyinstaller==5.1 call pyinstaller ../pyinstaller/picasso.spec -y --clean diff --git a/release/one_click_windows_gui/picasso_innoinstaller.iss b/release/one_click_windows_gui/picasso_innoinstaller.iss index 41e59d0c..bae93757 100644 --- a/release/one_click_windows_gui/picasso_innoinstaller.iss +++ b/release/one_click_windows_gui/picasso_innoinstaller.iss @@ -1,10 +1,10 @@ [Setup] AppName=Picasso AppPublisher=Jungmann Lab, Max Planck Institute of Biochemistry -AppVersion=0.5.7 +AppVersion=0.6.0 DefaultDirName={pf}\Picasso DefaultGroupName=Picasso -OutputBaseFilename="Picasso-Windows-64bit-0.5.7" +OutputBaseFilename="Picasso-Windows-64bit-0.6.0" ArchitecturesAllowed=x64 ArchitecturesInstallIn64BitMode=x64 diff --git a/setup.py b/setup.py index ea7580f3..195b42be 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name="picassosr", - version="0.5.7", + version="0.6.0", author="Joerg Schnitzbauer, Maximilian T. Strauss, Rafal Kowalewski", author_email=("joschnitzbauer@gmail.com, straussmaximilian@gmail.com, rafalkowalewski998@gmail.com"), url="https://github.com/jungmannlab/picasso", diff --git a/tests/data/testdata_locs.hdf5 b/tests/data/testdata_locs.hdf5 index b3f0af0a..71f8dd2e 100644 Binary files a/tests/data/testdata_locs.hdf5 and b/tests/data/testdata_locs.hdf5 differ diff --git a/tests/data/testdata_locs.yaml b/tests/data/testdata_locs.yaml index c4db63ef..8144504d 100644 --- a/tests/data/testdata_locs.yaml +++ b/tests/data/testdata_locs.yaml @@ -65,8 +65,10 @@ Structure.StructureY: 0,20,40,60,0,20,40,60,0,20,40,60 Width: 32 --- Box Size: 7 -Fit method: LQ, Gaussian +Convergence Criterion: 0.001 +Fit method: mle Generated by: Picasso Localize -Min. Net Gradient: 3000 +Max. Iterations: 1000 +Min. Net Gradient: 5000 Pixelsize: 130 ROI: null diff --git a/tests/test_localize.py b/tests/test_localize.py index 17df1cc0..80649ca0 100644 --- a/tests/test_localize.py +++ b/tests/test_localize.py @@ -27,6 +27,7 @@ def test_localize(): args.sensitivity = 1 args.gain = 1 args.qe = 1 + args.roi = None args.drift = 100 for fit_method in ["mle"]: