From 3e39e55835ee83e0b7f5825c7294845324f96802 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sun, 15 Dec 2024 14:49:16 +0100 Subject: [PATCH 01/14] one click installer correct python version for pyinstaller compatibility --- release/one_click_windows_gui/create_installer_windows.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/one_click_windows_gui/create_installer_windows.bat b/release/one_click_windows_gui/create_installer_windows.bat index 4be32332..7263ca15 100644 --- a/release/one_click_windows_gui/create_installer_windows.bat +++ b/release/one_click_windows_gui/create_installer_windows.bat @@ -5,7 +5,7 @@ call RMDIR /Q/S dist call cd %~dp0\..\.. -call conda create -n picasso_installer python=3.10 -y +call conda create -n picasso_installer python=3.10.15 -y call conda activate picasso_installer call python setup.py sdist bdist_wheel From 37a93de7b3917dddd1fcb860b3f227321ed616ca Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Wed, 22 Jan 2025 11:09:48 +0100 Subject: [PATCH 02/14] update plugin documentation --- changelog.rst | 6 +++++- docs/plugins.rst | 19 +++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/changelog.rst b/changelog.rst index 92401f3c..7265230e 100644 --- a/changelog.rst +++ b/changelog.rst @@ -1,7 +1,11 @@ Changelog ========= -Last change: 14-DEC-2024 MTS +Last change: 20-JAN-2025 MTS + +0.7.5 +----- +- Plugin docs update 0.7.1 - 0.7.4 ------------- diff --git a/docs/plugins.rst b/docs/plugins.rst index f9c8a5d3..4f5ee29b 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -4,34 +4,33 @@ Plugins Usage ----- -Starting in version 0.5.0, Picasso allows for creating plugins. They can be added by the user for each of the available distributions of picasso (GitHub, picassosr from PyPI and the one click installer). However, using plugins in the latter may cause issues, see below. +Starting in version 0.5.0, Picasso supports plugins. Please find the instructions on how to add them to your Picasso below. -Please keep in mind that the ``__init__.py`` file in the ``plugins`` folder must not be modified or deleted. +Please keep in mind that the ``__init__.py`` file in the ``picasso/picasso/gui/plugins`` folder must not be modified or deleted. 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/gui/plugins``. - Copy the plugin(s) to this folder. -- The plugin(s) should automatically work after running picasso in the new command window. PyPI ~~~~ -- Find the location of the environment where picassosr is installed (type ``conda env list`` to see the directory). -- Find this directory and go to ``YOUR_ENVIRONMENT/Lib/site-packages/picasso/gui/plugins``. +If you installed Picasso using ``pip install picassosr``, you can add plugins by following these steps: +- To find the location of the environment where ``picassosr`` is installed, type ``conda env list``. +- If your environment can be found under ``YOUR_ENVIRONMENT``, go to ``YOUR_ENVIRONMENT/Lib/site-packages/picasso/gui/plugins``. - Copy the plugin(s) to this folder. -- The plugin(s) should automatically work after running picasso in the new command window. One click installer ~~~~~~~~~~~~~~~~~~~ -**NOTE**: This may lead to issues if Picasso is installed, as the plugin scripts will remain in the ``plugins`` folder upon deinstallation. After deinstalltion, the ``Picasso`` folder needs to be deleted manually. +**NOTE**: After deinstalling Picasso, ``Program Files/Picasso`` folder needs 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:/Program Files/Picasso``. - Go to the following subfolder in the `Picasso` directory: ``picasso/gui/plugins``. - Copy the plugin(s) to this folder. -- The plugin(s) should automatically be loaded after double-clicking the respective desktop shortcuts. -**NOTE**: Plugins added in this distribution will not be able to use packages that are not installed automatically (from the file ``requirements.txt``). +.. **NOTE**: Plugins added in this distribution will not be able to use packages that are not installed automatically (from the file ``requirements.txt``). For developers -------------- @@ -41,4 +40,4 @@ To create a plugin, you can use the template provided in ``picasso/plugin_templa :scale: 70 % :alt: Plugins -As an example, please see any of the plugins on the GitHub `repo `_. \ No newline at end of file +As an example, please see any of the plugins in the `GitHub repo `_. \ No newline at end of file From d106289d792e3c6912ec5d00ab6491f6716d41c0 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Wed, 22 Jan 2025 13:10:54 +0100 Subject: [PATCH 03/14] Filter histogram bug fix - when IQR is zero, error was displayed --- changelog.rst | 1 + picasso/lib.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/changelog.rst b/changelog.rst index 7265230e..e63a47b8 100644 --- a/changelog.rst +++ b/changelog.rst @@ -6,6 +6,7 @@ Last change: 20-JAN-2025 MTS 0.7.5 ----- - Plugin docs update +- Bug fixes 0.7.1 - 0.7.4 ------------- diff --git a/picasso/lib.py b/picasso/lib.py index 6d15f16c..f9aab269 100644 --- a/picasso/lib.py +++ b/picasso/lib.py @@ -133,11 +133,11 @@ def calculate_optimal_bins(data, max_n_bins=None): if data.dtype.kind in ("u", "i") and bin_size < 1: bin_size = 1 bin_min = data.min() - bin_size / 2 - n_bins = _np.ceil((data.max() - bin_min) / bin_size) try: + n_bins = (data.max() - bin_min) / bin_size n_bins = int(n_bins) - except ValueError: - return None + except: + n_bins = 10 if max_n_bins and n_bins > max_n_bins: n_bins = max_n_bins bins = _np.linspace(bin_min, data.max(), n_bins) From c98a05851649d41fc0256876e4f36b132b3280a0 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Wed, 5 Feb 2025 18:07:11 +0100 Subject: [PATCH 04/14] add (h)dbscan references to the main page --- readme.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/readme.rst b/readme.rst index ea96bea2..ce15c029 100644 --- a/readme.rst +++ b/readme.rst @@ -135,10 +135,12 @@ If you use picasso in your research, please cite our Nature Protocols publicatio | | If you use some of the functionalities provided by Picasso, please also cite the respective publications: -- Nearest Neighbor based Analysis (NeNA) for experimental localization precision. DOI: `https://doi.org/10.1007/s00418-014-1192-3 `__ -- Theoretical localization precision (Gauss LQ and MLE). DOI: `https://doi.org/10.1038/nmeth.1447 `__ -- MLE fitting. DOI: `https://doi.org/10.1038/nmeth.1449 `__ +- Nearest Neighbor based Analysis (NeNA) for experimental localization precision. DOI: `10.1007/s00418-014-1192-3 `__ +- Theoretical localization precision (Gauss LQ and MLE). DOI: `10.1038/nmeth.1447 `__ +- MLE fitting. DOI: `10.1038/nmeth.1449 `__ - AIM undrifting. DOI: `10.1126/sciadv.adm776 `__ +- DBSCAN: Ester, et al. Inkdd, 1996. (Vol. 96, No. 34, pp. 226-231). +- HDBSCAN. DOI: `10.1007/978-3-642-37456-2_14 `__ Credits ------- From 7bfefa52d05fa4ac422e369f5983b7e8cc29361c Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 10:16:45 +0100 Subject: [PATCH 05/14] counting polygonal picks FIX --- picasso/gui/render.py | 14 +++++++++++--- picasso/lib.py | 8 ++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 471c118a..9629fc94 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -5785,7 +5785,7 @@ def add_point(self, position, update_scene=True): if update_scene: self.update_scene() - def add_polygon_point(self, point_movie, point_screen, update_scene=True): + def add_polygon_point(self, point_movie, point_screen): """Adds a new point to the polygon or closes the current polygon.""" @@ -5796,6 +5796,9 @@ def add_polygon_point(self, point_movie, point_screen, update_scene=True): # to be added if len(self._picks[-1]) < 3: # cannot close polygon yet self._picks[-1].append(point_movie) + # if the last polygon has been closed, start a new pick + elif self._picks[-1][0] == self._picks[-1][-1]: + self._picks.append([point_movie]) else: # check the distance between the current point and the # starting point of the currently drawn polygon @@ -5807,8 +5810,7 @@ def add_polygon_point(self, point_movie, point_screen, update_scene=True): # close the polygon if distance2 < POLYGON_POINTER_SIZE ** 2: self._picks[-1].append(self._picks[-1][0]) - self._picks.append([]) - else: # add a new point + else: # add a new point to the existing pick self._picks[-1].append(point_movie) self.update_pick_info_short() self.update_scene(picks_only=True) @@ -9205,6 +9207,12 @@ def save_picked_locs(self, path, channel): elif self._pick_shape == "Rectangle": w = self.window.tools_settings_dialog.pick_width.value() pick_info["Pick Width"] = w + # if polygon pick and the last not closed, ignore the last pick + elif ( + self._pick_shape == "Polygon" + and self._picks[-1][0] != self._picks[-1][-1] + ): + pick_info["Number of picks"] -= 1 io.save_locs(path, locs, self.infos[channel] + [pick_info]) def save_picked_locs_multi(self, path): diff --git a/picasso/lib.py b/picasso/lib.py index f9aab269..ea0aee48 100644 --- a/picasso/lib.py +++ b/picasso/lib.py @@ -629,14 +629,14 @@ def pick_areas_polygon(picks): Pick areas. """ - areas = _np.zeros(len(picks)) + areas = [] for i, pick in enumerate(picks): if len(pick) < 3 or pick[0] != pick[-1]: # not a closed polygon - areas[i] = 0 continue X, Y = get_pick_polygon_corners(pick) - areas[i] = polygon_area(X, Y) - areas = areas[areas > 0] # remove open polygons + areas.append(polygon_area(X, Y)) + areas = _np.array(areas) + areas = areas[areas > 0] # remove open polygons #TODO: delete this line? return areas From a5ea7184e9af7ae1a62f72e2e6d9e67d468b6f42 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 10:34:14 +0100 Subject: [PATCH 06/14] AIM docstrings and error message CLEAN --- picasso/aim.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/picasso/aim.py b/picasso/aim.py index 1010872d..9412c90e 100644 --- a/picasso/aim.py +++ b/picasso/aim.py @@ -628,7 +628,8 @@ def aim( Radius of the local search region in camera pixels. Should be larger than the maximum expected drift within segmentation. progress : picasso.lib.ProgressDialog (default=None) - Progress dialog. If None, progress is displayed with tqdm. + Progress dialog. If None, progress is displayed with into the + console. Returns ------- @@ -655,7 +656,10 @@ def aim( if val := inf.get("Pixelsize"): pixelsize = val if _np.isnan(width * height * pixelsize * n_frames): - raise KeyError("Insufficient metadata available.") + raise KeyError( + "Insufficient metadata available. Please specify 'Width', 'Height'," + " 'Frames' and 'Pixelsize' in the metadata .yaml." + ) # frames should start at 1 frame = locs["frame"] + 1 From 3dc7d5030e9db5e1e7707d5a9a200252db8541b3 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 10:38:14 +0100 Subject: [PATCH 07/14] AIM on locs with frame filtered FIX --- picasso/aim.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/picasso/aim.py b/picasso/aim.py index 9412c90e..95c04832 100644 --- a/picasso/aim.py +++ b/picasso/aim.py @@ -652,7 +652,7 @@ def aim( if val := inf.get("Height"): height = val if val := inf.get('Frames'): - n_frames = val + n_frames = val - locs["frame"].min() if val := inf.get("Pixelsize"): pixelsize = val if _np.isnan(width * height * pixelsize * n_frames): @@ -662,7 +662,8 @@ def aim( ) # frames should start at 1 - frame = locs["frame"] + 1 + frame = locs["frame"] + 1 - locs["frame"].min() + # find the segmentation bounds (temporal intervals) seg_bounds = _np.concatenate(( _np.arange(0, n_frames, segmentation), [n_frames] From 7e78353159a22e96658543763dd3c80540afe600 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 10:40:05 +0100 Subject: [PATCH 08/14] correct changelog --- changelog.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.rst b/changelog.rst index e63a47b8..449e322c 100644 --- a/changelog.rst +++ b/changelog.rst @@ -6,7 +6,9 @@ Last change: 20-JAN-2025 MTS 0.7.5 ----- - Plugin docs update -- Bug fixes +- Filter histogram display fixed for datasets with low variance (bug fix) +- AIM undrifting works now if the first frames of localizations are filtered out (bug fix) +- Other minor bug fixes 0.7.1 - 0.7.4 ------------- From c5111f33f739e656eb34a3564690d6a0cea8e931 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 10:42:56 +0100 Subject: [PATCH 09/14] 2D drift plot inverts y axis to match the rendered locs --- changelog.rst | 3 ++- picasso/gui/render.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/changelog.rst b/changelog.rst index 449e322c..45f5abc3 100644 --- a/changelog.rst +++ b/changelog.rst @@ -1,13 +1,14 @@ Changelog ========= -Last change: 20-JAN-2025 MTS +Last change: 06-FEB-2025 MTS 0.7.5 ----- - Plugin docs update - Filter histogram display fixed for datasets with low variance (bug fix) - AIM undrifting works now if the first frames of localizations are filtered out (bug fix) +- 2D drift plot in Render inverts y axis to match the rendered localizations - Other minor bug fixes 0.7.1 - 0.7.4 diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 9629fc94..5bb76b59 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -2897,6 +2897,7 @@ def plot_3d(self, drift): ax2.set_xlabel("x (nm)") ax2.set_ylabel("y (nm)") + ax2.inverse_yaxis() ax3 = self.figure.add_subplot(133) ax3.plot(drift.z, label="z") ax3.legend(loc="best") @@ -2937,6 +2938,7 @@ def plot_2d(self, drift): ax2.set_xlabel("x (nm)") ax2.set_ylabel("y (nm)") + ax2.invert_yaxis() self.canvas.draw() From cab680a55eb481a15afbb47ddab6942696bd9f58 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 13:24:54 +0100 Subject: [PATCH 10/14] 3d animation fix --- changelog.rst | 1 + docs/render.rst | 2 ++ picasso/gui/rotation.py | 4 ++-- requirements.txt | 1 - 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/changelog.rst b/changelog.rst index 45f5abc3..057334d6 100644 --- a/changelog.rst +++ b/changelog.rst @@ -9,6 +9,7 @@ Last change: 06-FEB-2025 MTS - Filter histogram display fixed for datasets with low variance (bug fix) - AIM undrifting works now if the first frames of localizations are filtered out (bug fix) - 2D drift plot in Render inverts y axis to match the rendered localizations +- 3D animation fixed - Other minor bug fixes 0.7.1 - 0.7.4 diff --git a/docs/render.rst b/docs/render.rst index 2b2d70c4..31b3003d 100644 --- a/docs/render.rst +++ b/docs/render.rst @@ -61,6 +61,8 @@ The 3D rotation window allows the user to render 3D localization data. To use it The user may perform multiple actions in the rotation window, including: saving rotated localizations, building animations (.mp4 format), rotating by a specified angle, etc. +Note that to build animations, the user must have ``ffmpeg`` installed on their system. + Rotation around z-axis is available by pressing Ctrl/Command. Rotation axis can be frozen by pressing x/y/z to freeze around the corresponding axes (to freeze around the z-axis, Ctrl/Command must be pressed as well). 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. diff --git a/picasso/gui/rotation.py b/picasso/gui/rotation.py index 39dbcc09..f4e3a2c8 100644 --- a/picasso/gui/rotation.py +++ b/picasso/gui/rotation.py @@ -16,8 +16,7 @@ import numpy as np import matplotlib.pyplot as plt -# from moviepy.video.io.ImageSequenceClip import ImageSequenceClip -import imageio +import imageio.v2 as imageio from PyQt5 import QtCore, QtGui, QtWidgets from numpy.lib.recfunctions import stack_arrays @@ -612,6 +611,7 @@ def build_animation(self): height += 1 # render all frames and save in RAM + # video_writer = imageio.get_writer(name, fps=self.fps.value(),codec='libx264', format='FFMPEG') video_writer = imageio.get_writer(name, fps=self.fps.value()) progress = lib.ProgressDialog( "Rendering frames", 0, len(angx), self.window diff --git a/requirements.txt b/requirements.txt index 686cc129..9d1fa4c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,6 @@ scikit-learn==1.3.1 tqdm==4.66.1 lmfit==1.2.2 streamlit==1.27.0 -moviepy==1.0.3 nd2==0.7.2 sqlalchemy==2.0.21 plotly-express==0.4.1 From d8bd2d21c8623605b39b590f0790937ead6185fe Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 14:24:58 +0100 Subject: [PATCH 11/14] pick fiducials ADD --- changelog.rst | 1 + picasso/gui/render.py | 33 +++++++++++++++++++++++++ picasso/imageprocess.py | 54 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/changelog.rst b/changelog.rst index 057334d6..23c7798b 100644 --- a/changelog.rst +++ b/changelog.rst @@ -5,6 +5,7 @@ Last change: 06-FEB-2025 MTS 0.7.5 ----- +- Automatic picking of fiducials added in Render: ``Tools/Pick fiducials`` - Plugin docs update - Filter histogram display fixed for datasets with low variance (bug fix) - AIM undrifting works now if the first frames of localizations are filtered out (bug fix) diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 5bb76b59..2b2f5475 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -5428,6 +5428,8 @@ class View(QtWidgets.QLabel): Moves viewport by a given relative distance pick_areas() Finds the areas of all current picks in um^2. + pick_fiducials() + Finds the circular picks centered around the fiducials pick_message_box(params) Returns a message box for selecting picks pick_similar() @@ -8597,6 +8599,34 @@ def pick_areas(self): areas *= (pixelsize * 1e-3) ** 2 # convert to um^2 return areas + def pick_fiducials(self): + """Finds the circular picks centered around the fiducials.""" + + channel = self.get_channel("Pick fiducials") + if channel is None: + return + + if self._pick_shape != "Circle": + message = "Please select circular pick before picking fiducials." + QtWidgets.QMessageBox.warning(self, "Warning", message) + return + if len(self._picks): + message = "Please remove all picks before picking fiducials." + QtWidgets.QMessageBox.warning(self, "Warning", message) + return + + locs = self.all_locs[channel] + info = self.infos[channel] + picks, box = imageprocess.find_fiducials(locs, info) + + if len(picks) == 0: + message = "No fiducials found, manual picking is required." + QtWidgets.QMessageBox.warning(self, "Warning", message) + return + + self.window.tools_settings_dialog.pick_diameter.setValue(box) + self.add_picks(picks) + @check_picks def pick_similar(self): """ @@ -10951,6 +10981,9 @@ def initUI(self, plugins_loaded): move_to_pick_action = tools_menu.addAction("Move to pick") move_to_pick_action.triggered.connect(self.view.move_to_pick) + pick_fiducials_action = tools_menu.addAction("Pick fiducials") + pick_fiducials_action.triggered.connect(self.view.pick_fiducials) + tools_menu.addSeparator() show_trace_action = tools_menu.addAction("Show trace") show_trace_action.setShortcut("Ctrl+R") diff --git a/picasso/imageprocess.py b/picasso/imageprocess.py index 6b63aefd..1259c3a1 100644 --- a/picasso/imageprocess.py +++ b/picasso/imageprocess.py @@ -13,7 +13,8 @@ import lmfit as _lmfit from tqdm import tqdm as _tqdm from . import lib as _lib - +from . import render as _render +from . import localize as _localize _plt.style.use("ggplot") @@ -132,3 +133,54 @@ def rcc(segments, max_shift=None, callback=None): callback(flag) return _lib.minimize_shifts(shifts_x, shifts_y) + + +def find_fiducials(locs, info): + """Finds the xy coordinates of regions with high density of + localizations, likely originating from fiducial markers. + + Uses picasso.localize.identify_in_image with threshold set to 99th + percentile of the image histogram. The image is rendered using + one-pixel-blur, see picasso.render.render. + + + Parameters + ---------- + locs : np.recarray + Localizations. + info : list of dicts + Localizations' metadata (from the corresponding .yaml file). + + Returns + ------- + picks : list of (2,) tuples + Coordinates of fiducial markers. Each list element corresponds + to (x, y) coordinates of one fiducial marker. + box : int + Size of the box used for the fiducial marker identification. + Can be set as the pick diameter in pixels for undrifting. + """ + + image = _render.render( + locs=locs, + info=info, + oversampling=1, + viewport=None, + blur_method="smooth", + )[1] + hist = _np.histogram(image.flatten(), bins=256) + threshold = _np.percentile(hist[0], 99) + # box size should be an odd number, corresponding to approximately + # 900 nm + pixelsize = 130 + for inf in info: + if val := inf.get("Pixelsize"): + pixelsize = val + break + box = int(_np.round(900 / pixelsize)) + box = box + 1 if box % 2 == 0 else box + + # find the local maxima and translate to pick coordinates + y, x, _ = _localize.identify_in_image(image, threshold, box=box) + picks = [(xi, yi) for xi, yi in zip(x, y)] + return picks, box \ No newline at end of file From 8f976aed16760b5e5526319c2ec539c4c0972777 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 15:36:57 +0100 Subject: [PATCH 12/14] move undrift from picked to picasso.postprocess (formerly in picasso.gui.render.View) --- picasso/gui/render.py | 110 ++++++----------------------------------- picasso/postprocess.py | 75 ++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 94 deletions(-) diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 2b2f5475..d28a9b88 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -5532,8 +5532,6 @@ class View(QtWidgets.QLabel): Undrifts with AIM. undrift_from_picked() Undrifts from picked localizations. - _undrift_from_picked_coordinate() - Calculates drift in a given coordinate undrift_from_picked2d() Undrifts x and y coordinates from picked localizations. undrift_rcc() @@ -9867,31 +9865,19 @@ def undrift_from_picked(self): picked_locs = self.picked_locs(channel) status = lib.StatusDialog("Calculating drift...", self) - drift_x = self._undrift_from_picked_coordinate( - channel, picked_locs, "x" - ) # find drift in x - drift_y = self._undrift_from_picked_coordinate( - channel, picked_locs, "y" - ) # find drift in y + drift = postprocess.undrift_from_picked( + picked_locs, self.infos[channel] + ) # Apply drift - self.all_locs[channel].x -= drift_x[self.all_locs[channel].frame] - self.all_locs[channel].y -= drift_y[self.all_locs[channel].frame] - self.locs[channel].x -= drift_x[self.locs[channel].frame] - self.locs[channel].y -= drift_y[self.locs[channel].frame] - - # A rec array to store the applied drift - drift = (drift_x, drift_y) - drift = np.rec.array(drift, dtype=[("x", "f"), ("y", "f")]) - + self.all_locs[channel].x -= drift["x"][self.all_locs[channel].frame] + self.all_locs[channel].y -= drift["y"][self.all_locs[channel].frame] + self.locs[channel].x -= drift["x"][self.locs[channel].frame] + self.locs[channel].y -= drift["y"][self.locs[channel].frame] # If z coordinate exists, also apply drift there if all([hasattr(_, "z") for _ in picked_locs]): - drift_z = self._undrift_from_picked_coordinate( - channel, picked_locs, "z" - ) - self.all_locs[channel].z -= drift_z[self.all_locs[channel].frame] - self.locs[channel].z -= drift_z[self.locs[channel].frame] - drift = lib.append_to_rec(drift, drift_z, "z") + self.all_locs[channel].z -= drift["z"][self.all_locs[channel].frame] + self.locs[channel].z -= drift["z"][self.locs[channel].frame] # Cleanup self.index_blocks[channel] = None @@ -9909,85 +9895,21 @@ def undrift_from_picked2d(self): picked_locs = self.picked_locs(channel) status = lib.StatusDialog("Calculating drift...", self) - drift_x = self._undrift_from_picked_coordinate( - channel, picked_locs, "x" + drift = postprocess.undrift_from_picked( + picked_locs, self.infos[channel] ) - drift_y = self._undrift_from_picked_coordinate( - channel, picked_locs, "y" - ) - - # Apply drift - self.all_locs[channel].x -= drift_x[self.all_locs[channel].frame] - self.all_locs[channel].y -= drift_y[self.all_locs[channel].frame] - self.locs[channel].x -= drift_x[self.locs[channel].frame] - self.locs[channel].y -= drift_y[self.locs[channel].frame] - # A rec array to store the applied drift - drift = (drift_x, drift_y) - drift = np.rec.array(drift, dtype=[("x", "f"), ("y", "f")]) + # Apply drift, ignore z coordinates + self.all_locs[channel].x -= drift["x"][self.all_locs[channel].frame] + self.all_locs[channel].y -= drift["y"][self.all_locs[channel].frame] + self.locs[channel].x -= drift["x"][self.locs[channel].frame] + self.locs[channel].y -= drift["y"][self.locs[channel].frame] # Cleanup self.index_blocks[channel] = None self.add_drift(channel, drift) status.close() self.update_scene() - - def _undrift_from_picked_coordinate( - self, channel, picked_locs, coordinate - ): - """ - Calculates drift in a given coordinate. - - Parameters - ---------- - channel : int - Channel where locs are being undrifted - picked_locs : list - List of np.recarrays with locs for each pick - coordinate : str - Spatial coordinate where drift is to be found - - Returns - ------- - np.array - Contains average drift across picks for all frames - """ - - n_picks = len(picked_locs) - n_frames = self.infos[channel][0]["Frames"] - - # Drift per pick per frame - drift = np.empty((n_picks, n_frames)) - drift.fill(np.nan) - - # Remove center of mass offset - for i, locs in enumerate(picked_locs): - coordinates = getattr(locs, coordinate) - drift[i, locs.frame] = coordinates - np.mean(coordinates) - - # Mean drift over picks - drift_mean = np.nanmean(drift, 0) - # Square deviation of each pick's drift to mean drift along frames - sd = (drift - drift_mean) ** 2 - # Mean of square deviation for each pick - msd = np.nanmean(sd, 1) - # New mean drift over picks - # where each pick is weighted according to its msd - nan_mask = np.isnan(drift) - drift = np.ma.MaskedArray(drift, mask=nan_mask) - drift_mean = np.ma.average(drift, axis=0, weights=1/msd) - drift_mean = drift_mean.filled(np.nan) - - # Linear interpolation for frames without localizations - def nan_helper(y): - return np.isnan(y), lambda z: z.nonzero()[0] - - nans, nonzero = nan_helper(drift_mean) - drift_mean[nans] = np.interp( - nonzero(nans), nonzero(~nans), drift_mean[~nans] - ) - - return drift_mean def undo_drift(self): """ Gets channel for undoing drift. """ diff --git a/picasso/postprocess.py b/picasso/postprocess.py index af74d7d9..41f1d891 100644 --- a/picasso/postprocess.py +++ b/picasso/postprocess.py @@ -1227,6 +1227,8 @@ def undrift( segmentation_callback=None, rcc_callback=None, ): + """Undrift by RCC. """ + bounds, segments = segment( locs, info, @@ -1286,6 +1288,79 @@ def undrift( return drift, locs +def undrift_from_picked(picked_locs, info): + """Finds drift from picked localizations. Note that unlike other + undrifting functions, this function does not return undrifted + localizations but only drift.""" + + drift_x = _undrift_from_picked_coordinate(picked_locs, info, "x") + drift_y = _undrift_from_picked_coordinate(picked_locs, info, "y") + + # A rec array to store the applied drift + drift = (drift_x, drift_y) + drift = _np.rec.array(drift, dtype=[("x", "f"), ("y", "f")]) + + # If z coordinate exists, also apply drift there + if all([hasattr(_, "z") for _ in picked_locs]): + drift_z = _undrift_from_picked_coordinate(picked_locs, info, "z") + drift = _lib.append_to_rec(drift, drift_z, "z") + return drift + + +def _undrift_from_picked_coordinate(picked_locs, info, coordinate): + """Calculates drift in a given coordinate. + + Parameters + ---------- + picked_locs : list + List of np.recarrays with locs for each pick. + info : list of dicts + Localizations' metadeta. + coordinate : {"x", "y", "z"} + Spatial coordinate where drift is to be found. + + Returns + ------- + drift_mean : np.array + Average drift across picks for all frames + """ + + n_picks = len(picked_locs) + n_frames = info[0]["Frames"] + + # Drift per pick per frame + drift = _np.empty((n_picks, n_frames)) + drift.fill(_np.nan) + + # Remove center of mass offset + for i, locs in enumerate(picked_locs): + coordinates = getattr(locs, coordinate) + drift[i, locs.frame] = coordinates - _np.mean(coordinates) + + # Mean drift over picks + drift_mean = _np.nanmean(drift, 0) + # Square deviation of each pick's drift to mean drift along frames + sd = (drift - drift_mean) ** 2 + # Mean of square deviation for each pick + msd = _np.nanmean(sd, 1) + # New mean drift over picks + # where each pick is weighted according to its msd + nan_mask = _np.isnan(drift) + drift = _np.ma.MaskedArray(drift, mask=nan_mask) + drift_mean = _np.ma.average(drift, axis=0, weights=1/msd) + drift_mean = drift_mean.filled(_np.nan) + + # Linear interpolation for frames without localizations + def nan_helper(y): + return _np.isnan(y), lambda z: z.nonzero()[0] + + nans, nonzero = nan_helper(drift_mean) + drift_mean[nans] = _np.interp( + nonzero(nans), nonzero(~nans), drift_mean[~nans] + ) + return drift_mean + + def align(locs, infos, display=False): images = [] for i, (locs_, info_) in enumerate(zip(locs, infos)): From 0494b8508ccfe916fe0cdd2c85466af54448d310 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 15:38:00 +0100 Subject: [PATCH 13/14] =?UTF-8?q?Bump=20version:=200.7.4=20=E2=86=92=200.7?= =?UTF-8?q?.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- distribution/picasso.iss | 4 ++-- docs/conf.py | 2 +- picasso/__init__.py | 2 +- picasso/__version__.py | 2 +- release/one_click_windows_gui/create_installer_windows.bat | 2 +- release/one_click_windows_gui/picasso_innoinstaller.iss | 4 ++-- setup.py | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 44b55258..359f980d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.7.4 +current_version = 0.7.5 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? diff --git a/distribution/picasso.iss b/distribution/picasso.iss index 97078804..c202dba0 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.7.4 +AppVersion=0.7.5 DefaultDirName={commonpf}\Picasso DefaultGroupName=Picasso -OutputBaseFilename="Picasso-Windows-64bit-0.7.4" +OutputBaseFilename="Picasso-Windows-64bit-0.7.5" ArchitecturesAllowed=x64 ArchitecturesInstallIn64BitMode=x64 diff --git a/docs/conf.py b/docs/conf.py index 3c1d6945..1b483f98 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.7.4" +release = "0.7.5" # -- General configuration --------------------------------------------------- diff --git a/picasso/__init__.py b/picasso/__init__.py index eda0e15f..daee83f6 100644 --- a/picasso/__init__.py +++ b/picasso/__init__.py @@ -8,7 +8,7 @@ import os.path as _ospath import yaml as _yaml -__version__ = "0.7.4" +__version__ = "0.7.5" _this_file = _ospath.abspath(__file__) _this_dir = _ospath.dirname(_this_file) diff --git a/picasso/__version__.py b/picasso/__version__.py index d1f00358..0fab7c7f 100644 --- a/picasso/__version__.py +++ b/picasso/__version__.py @@ -1 +1 @@ -VERSION_NO = "0.7.4" +VERSION_NO = "0.7.5" diff --git a/release/one_click_windows_gui/create_installer_windows.bat b/release/one_click_windows_gui/create_installer_windows.bat index 7263ca15..8eba48cc 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.7.4-py3-none-any.whl" +call pip install "../../dist/picassosr-0.7.5-py3-none-any.whl" call pip install pyinstaller==5.12 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 6dd6d2d4..cfffc8f2 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.7.4 +AppVersion=0.7.5 DefaultDirName={commonpf}\Picasso DefaultGroupName=Picasso -OutputBaseFilename="Picasso-Windows-64bit-0.7.4" +OutputBaseFilename="Picasso-Windows-64bit-0.7.5" ArchitecturesAllowed=x64 ArchitecturesInstallIn64BitMode=x64 diff --git a/setup.py b/setup.py index 433cf9a7..b2f40857 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name="picassosr", - version="0.7.4", + version="0.7.5", 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", From d891330ca277fda8759a5100f5287996642f92ef Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 15:39:10 +0100 Subject: [PATCH 14/14] CLEAN --- changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.rst b/changelog.rst index 23c7798b..c0a052e3 100644 --- a/changelog.rst +++ b/changelog.rst @@ -6,6 +6,7 @@ Last change: 06-FEB-2025 MTS 0.7.5 ----- - Automatic picking of fiducials added in Render: ``Tools/Pick fiducials`` +- Undrifting from picked moved from ``picasso/gui/render`` to ``picasso/postprocess`` - Plugin docs update - Filter histogram display fixed for datasets with low variance (bug fix) - AIM undrifting works now if the first frames of localizations are filtered out (bug fix)