From 402a646e8f9adad764c80dd05a9bba465e095c4c Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Tue, 15 Oct 2024 18:26:27 +0200 Subject: [PATCH 01/17] RESI dialog FIX, units in nm --- changelog.rst | 4 ++-- picasso/gui/render.py | 48 ++++++++++++++++++++++--------------------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/changelog.rst b/changelog.rst index 9626d88..c25917b 100644 --- a/changelog.rst +++ b/changelog.rst @@ -1,9 +1,9 @@ Changelog ========= -Last change: 02-OCT-2024 MTS +Last change: 15-OCT-2024 MTS -0.7.1 - 0.7.3 +0.7.1 - 0.7.4 ------------- - SMLM clusterer in picked regions deleted - Show legend in Render property displayed rounded tick label values diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 6ebd49e..54dbf7d 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -3933,7 +3933,7 @@ def __init__(self, window, tools_settings_dialog): class RESIDialog(QtWidgets.QDialog): - """ RESI dialog. + """RESI dialog. Allows for clustering multiple channels with user-defined clustering parameters using the SMLM clusterer; saves cluster @@ -4023,17 +4023,17 @@ def __init__(self, window): params_grid.addWidget(QtWidgets.QLabel("RESI channel"), 2, 0) if self.ndim == 2: params_grid.addWidget( - QtWidgets.QLabel("Radius\n[cam. pixel]"), 2, 1 + QtWidgets.QLabel("Radius\n[nm]"), 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 + QtWidgets.QLabel("Radius xy\n[nm]"), 2, 1 ) params_grid.addWidget( - QtWidgets.QLabel("Radius z\n[cam. pixel]"), 2, 2 + QtWidgets.QLabel("Radius z\n[nm]"), 2, 2 ) params_grid.addWidget( QtWidgets.QLabel("Min # localizations"), 2, 3 @@ -4045,17 +4045,17 @@ def __init__(self, window): 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) + r_xy.setRange(0.01, 1e6) + r_xy.setDecimals(2) + r_xy.setValue(10) + r_xy.setSingleStep(0.1) 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) + r_z.setRange(0.01, 1e6) + r_z.setDecimals(2) + r_z.setValue(25) + r_z.setSingleStep(0.1) self.radius_z.append(r_z) min_locs = QtWidgets.QSpinBox() @@ -4129,13 +4129,14 @@ def perform_resi(self): return ### Prepare data + # get camera pixel size + pixelsize = self.window.display_settings_dlg.pixelsize.value() + # extract clustering parameters - r_xy = [_.value() for _ in self.radius_xy] - r_z = [_.value() for _ in self.radius_z] + r_xy = [_.value() / pixelsize for _ in self.radius_xy] + r_z = [_.value() / pixelsize 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 @@ -4191,13 +4192,14 @@ def perform_resi(self): 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) + clustered_locs = clusterer.cluster( + locs, + radius_xy=r_xy[i], + min_locs=min_locs[i], + frame_analysis=apply_fa, + radius_z=r_z[i] if self.ndim == 3 else None, + pixelsize=pixelsize, + ) # save clustered localizations if requested if ok1: From 6d49c5247bbee8cb218038a4ed8b5fbdf4cf5dbd Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Tue, 15 Oct 2024 18:35:38 +0200 Subject: [PATCH 02/17] display the last loaded file in Render after closing a channel --- picasso/gui/render.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 54dbf7d..02bbcdc 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -826,6 +826,12 @@ def close_file(self, i, render=True): self.update_viewport() self.adjustSize() + # update the window title + self.window.setWindowTitle( + f"Picasso v{__version__}: Render. File: " + f"{os.path.basename(self.window.view.locs_paths[-1])}" + ) + def update_viewport(self): """ Updates the scene in the main window. """ @@ -5603,8 +5609,7 @@ def get_group_color(self, locs): return locs.group.astype(int) % N_GROUP_COLORS def add(self, path, render=True): - """ - Loads a .hdf5 localizations and the associated .yaml metadata + """Loads a .hdf5 localizations and the associated .yaml metadata files. Parameters @@ -5706,9 +5711,7 @@ def add(self, path, render=True): self.window.dataset_dialog.add_entry(path) self.window.setWindowTitle( - "Picasso v{}: Render. File: {}".format( - __version__, os.path.basename(path) - ) + f"Picasso v{__version__}: Render. File: {os.path.basename(path)}" ) # fast rendering add channel From d168a3ae618b516eea5d6392f5977b6ad976bcca Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Fri, 6 Dec 2024 09:07:01 +0100 Subject: [PATCH 03/17] CLEAN; FIX - saved picked locs metadata total area for circular pick --- changelog.rst | 10 ++++++++-- picasso/gui/render.py | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/changelog.rst b/changelog.rst index c25917b..8b36403 100644 --- a/changelog.rst +++ b/changelog.rst @@ -1,9 +1,15 @@ Changelog ========= -Last change: 15-OCT-2024 MTS +Last change: 06-DEC-2024 MTS -0.7.1 - 0.7.4 +0.7.4 +----- +- Picasso: Render's title bar displays the file names of only opened files +- Picasso: Render - RESI dialog fixed, units in nm +- Other minor bug fixes + +0.7.1 - 0.7.3 ------------- - SMLM clusterer in picked regions deleted - Show legend in Render property displayed rounded tick label values diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 02bbcdc..5199cf4 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -9118,6 +9118,10 @@ def save_picked_locs(self, path, channel): if self._pick_shape == "Circle": d = self.window.tools_settings_dialog.pick_diameter.value() pick_info["Pick Diameter"] = d + # correct for the total area + pick_info["Total Picked Area (um^2)"] = ( + pick_info["Total Picked Area (um^2)"] * len(self._picks) + ) elif self._pick_shape == "Rectangle": w = self.window.tools_settings_dialog.pick_width.value() pick_info["Pick Width"] = w From e7bef7e71e24031cf6b0cdad7f5c15a198a561b7 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Fri, 6 Dec 2024 09:16:20 +0100 Subject: [PATCH 04/17] CMD localize saves camera information in the metadata file --- changelog.rst | 1 + picasso/__main__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.rst b/changelog.rst index 8b36403..3249735 100644 --- a/changelog.rst +++ b/changelog.rst @@ -7,6 +7,7 @@ Last change: 06-DEC-2024 MTS ----- - Picasso: Render's title bar displays the file names of only opened files - Picasso: Render - RESI dialog fixed, units in nm +- CMD localize saves camera information in the metadata file - Other minor bug fixes 0.7.1 - 0.7.3 diff --git a/picasso/__main__.py b/picasso/__main__.py index 6e8327a..fbfee93 100644 --- a/picasso/__main__.py +++ b/picasso/__main__.py @@ -985,6 +985,7 @@ def prompt_info(): print("------------------------------------------") info.append(localize_info) + info.append(camera_info) base, ext = splitext(path) From 77675bec1f9eb7474bd765a8972a5ab69a43f6e3 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Fri, 6 Dec 2024 09:27:21 +0100 Subject: [PATCH 05/17] Render show drift in nm --- changelog.rst | 1 + picasso/gui/render.py | 37 ++++++++++++++++++++++--------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/changelog.rst b/changelog.rst index 3249735..417c158 100644 --- a/changelog.rst +++ b/changelog.rst @@ -7,6 +7,7 @@ Last change: 06-DEC-2024 MTS ----- - Picasso: Render's title bar displays the file names of only opened files - Picasso: Render - RESI dialog fixed, units in nm +- Picasso: Render - show drift in nm, not camera pixels - CMD localize saves camera information in the metadata file - Other minor bug fixes diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 5199cf4..a6c5ff0 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -2857,8 +2857,9 @@ class DriftPlotWindow(QtWidgets.QTabWidget): Creates 3 plots with drift """ - def __init__(self, info_dialog): + def __init__(self, parent): super().__init__() + self.parent = parent self.setWindowTitle("Drift Plot") this_directory = os.path.dirname(os.path.realpath(__file__)) icon_path = os.path.join(this_directory, "icons", "render.ico") @@ -2886,23 +2887,26 @@ def plot_3d(self, drift): self.figure.clear() + # get camera pixel size in nm + pixelsize = self.parent.window.display_settings_dlg.pixelsize.value() + ax1 = self.figure.add_subplot(131) - ax1.plot(drift.x, label="x") - ax1.plot(drift.y, label="y") + ax1.plot(drift.x * pixelsize, label="x") + ax1.plot(drift.y * pixelsize, label="y") ax1.legend(loc="best") ax1.set_xlabel("Frame") - ax1.set_ylabel("Drift (pixel)") + ax1.set_ylabel("Drift (nm)") ax2 = self.figure.add_subplot(132) ax2.plot( - drift.x, - drift.y, + drift.x * pixelsize, + drift.y * pixelsize, color=list(plt.rcParams["axes.prop_cycle"])[2][ "color" ], ) - ax2.set_xlabel("x") - ax2.set_ylabel("y") + ax2.set_xlabel("x (nm)") + ax2.set_ylabel("y (nm)") ax3 = self.figure.add_subplot(133) ax3.plot(drift.z, label="z") ax3.legend(loc="best") @@ -2923,23 +2927,26 @@ def plot_2d(self, drift): self.figure.clear() + # get camera pixel size in nm + pixelsize = self.parent.window.display_settings_dlg.pixelsize.value() + ax1 = self.figure.add_subplot(121) - ax1.plot(drift.x, label="x") - ax1.plot(drift.y, label="y") + ax1.plot(drift.x * pixelsize, label="x") + ax1.plot(drift.y * pixelsize, label="y") ax1.legend(loc="best") ax1.set_xlabel("Frame") - ax1.set_ylabel("Drift (pixel)") + ax1.set_ylabel("Drift (nm)") ax2 = self.figure.add_subplot(122) ax2.plot( - drift.x, - drift.y, + drift.x * pixelsize, + drift.y * pixelsize, color=list(plt.rcParams["axes.prop_cycle"])[2][ "color" ], ) - ax2.set_xlabel("x") - ax2.set_ylabel("y") + ax2.set_xlabel("x (nm)") + ax2.set_ylabel("y (nm)") self.canvas.draw() From a47615b205a373b716a375f2d24adc57a4172176 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Fri, 6 Dec 2024 11:34:01 +0100 Subject: [PATCH 06/17] Picasso: Render - masking localizations saves the mask area in metadata --- changelog.rst | 1 + picasso/gui/render.py | 180 +++++++++++++++++++++++------------------- 2 files changed, 98 insertions(+), 83 deletions(-) diff --git a/changelog.rst b/changelog.rst index 417c158..c432a1b 100644 --- a/changelog.rst +++ b/changelog.rst @@ -8,6 +8,7 @@ Last change: 06-DEC-2024 MTS - Picasso: Render's title bar displays the file names of only opened files - Picasso: Render - RESI dialog fixed, units in nm - Picasso: Render - show drift in nm, not camera pixels +- Picasso: Render - masking localizations saves the mask area in metadata - CMD localize saves camera information in the metadata file - Other minor bug fixes diff --git a/picasso/gui/render.py b/picasso/gui/render.py index a6c5ff0..ee4d7d2 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -3480,6 +3480,9 @@ class MaskSettingsDialog(QtWidgets.QDialog): Blurs localizations using a Gaussian filter generate_image() Histograms loaded localizations from a given channel + get_info(channel) + Returns metadata for saving masked localizaitons in a given + channel init_dialog() Initializes dialog when called from the main window load_mask() @@ -3490,12 +3493,14 @@ class MaskSettingsDialog(QtWidgets.QDialog): Masks localizations from a single or all channels _mask_locs(locs) Masks locs given a mask + save_blur() + Saves blurred image of localizations in .png format save_mask() Saves binary mask into .npy format save_locs() Saves masked localizations - save_locs_multi() - Saves masked localizations for all loaded channels + _save_locs(channel, path_in, path_out) + Saves masked localizations from a single channel update_plots() Plots in all 4 axes """ @@ -3544,10 +3549,7 @@ def __init__(self, window): mask_grid.addWidget(self.mask_thresh, 2, 1, 1, 2) gridspec_dict = { - 'bottom': 0.05, - 'top': 0.95, - 'left': 0.05, - 'right': 0.95, + 'bottom': 0.05, 'top': 0.95, 'left': 0.05, 'right': 0.95, } ( self.figure, @@ -3807,93 +3809,105 @@ def save_locs(self): """ Saves masked localizations. """ if self.save_all.isChecked(): # save all channels - self.save_locs_multi() - else: - out_path = self.paths[self.channel].replace( + suffix_in, ok1 = QtWidgets.QInputDialog.getText( + self, + "", + "Enter suffix for localizations inside the mask", + QtWidgets.QLineEdit.Normal, + "_mask_in", + ) + if ok1: + suffix_out, ok2 = QtWidgets.QInputDialog.getText( + self, + "", + "Enter suffix for localizations outside the mask", + QtWidgets.QLineEdit.Normal, + "_mask_out", + ) + if ok2: + for channel in range(len(self.index_locs)): + path_in = self.paths[channel].replace( + ".hdf5", f"{suffix_in}.hdf5" + ) + path_out = self.paths[channel].replace( + ".hdf5", f"{suffix_out}.hdf5" + ) + self._save_locs(channel, path_in, path_out) + + else: # save only the current channel + path_in = self.paths[self.channel].replace( ".hdf5", "_mask_in.hdf5" ) - path, ext = QtWidgets.QFileDialog.getSaveFileName( + path_in, ext = QtWidgets.QFileDialog.getSaveFileName( self, "Save localizations within mask", - out_path, + path_in, filter="*.hdf5", ) - if path: - info = self.infos[self.channel] + [ - { - "Generated by": "Picasso Render : Mask in ", - "Display pixel size [nm]": self.disp_px_size.value(), - "Blur": self.mask_blur.value(), - "Threshold": self.mask_thresh.value(), - } - ] - io.save_locs(path, self.index_locs[0], info) + if path_in: + path_out = self.paths[self.channel].replace( + ".hdf5", "_mask_out.hdf5" + ) + path_out, ext = QtWidgets.QFileDialog.getSaveFileName( + self, + "Save localizations outside of mask", + path_out, + filter="*.hdf5", + ) + if path_out: + self._save_locs(self.channel, path_in, path_out) + + def _save_locs(self, channel, path_in, path_out): + """ + Saves masked localizations for a single channel. - out_path = self.paths[self.channel].replace( - ".hdf5", "_mask_out.hdf5" - ) - path, ext = QtWidgets.QFileDialog.getSaveFileName( - self, - "Save localizations outside of mask", - out_path, - filter="*.hdf5", - ) - if path: - info = self.infos[self.channel] + [ - { - "Generated by": "Picasso Render : Mask out", - "Display pixel size [nm]": self.disp_px_size.value(), - "Blur": self.mask_blur.value(), - "Threshold": self.mask_thresh.value(), - } - ] - io.save_locs(path, self.index_locs_out[0], info) + Parameters + ---------- + channel : int + Channel of localizations to be saved + path_in : str + Path to save localizations inside the mask + path_out : str + Path to save localizations outside the mask + """ - def save_locs_multi(self): - """ Saves masked localizations for all loaded channels. """ + info = self.get_info(channel, locs_in=True) + io.save_locs(path_in, self.index_locs[channel], info) + info = self.get_info(channel, locs_in=False) + io.save_locs(path_out, self.index_locs_out[channel], info) - suffix_in, ok1 = QtWidgets.QInputDialog.getText( - self, - "", - "Enter suffix for localizations inside the mask", - QtWidgets.QLineEdit.Normal, - "_mask_in", - ) - if ok1: - suffix_out, ok2 = QtWidgets.QInputDialog.getText( - self, - "", - "Enter suffix for localizations outside the mask", - QtWidgets.QLineEdit.Normal, - "_mask_out", - ) - if ok2: - for channel in range(len(self.index_locs)): - out_path = self.paths[channel].replace( - ".hdf5", f"{suffix_in}.hdf5" - ) - info = self.infos[channel] + [ - { - "Generated by": "Picasso Render : Mask in", - "Display pixel size [nm]": self.disp_px_size.value(), - "Blur": self.mask_blur.value(), - "Threshold": self.mask_thresh.value(), - } - ] - io.save_locs(out_path, self.index_locs[channel], info) + def get_info(self, channel, locs_in=True): + """ + Returns metadata for masked localizations. + + Parameters + ---------- + channel : int + Channel of localizations to be saved + locs_in : bool (default=True) + True if localizations inside the mask are to be saved + + Returns + ------- + info : list of dicts + Metadata for masked localizations + """ + + mask_in = "in" if locs_in else "out" + mask_pixelsize = self.disp_px_size.value() + area_in = float(np.sum(self.mask)) * (mask_pixelsize * 1e-3) ** 2 + area_total = float(self.mask.size * (mask_pixelsize * 1e-3) ** 2) + area = area_in if locs_in else area_total - area_in + info = self.infos[channel] + [{ + "Generated by": f"Picasso Render : Mask {mask_in}", + "Display pixel size (nm)": mask_pixelsize, + "Blur": self.mask_blur.value(), + "Threshold": self.mask_thresh.value(), + "Area (um^2)": area, + # "Area (um^2)": np.sum(self.mask) * self.x_max * self.y_max, #TODO: get the right formula + }] + return info - out_path = self.paths[channel].replace( - ".hdf5", f"{suffix_out}.hdf5" - ) - info = self.infos[channel] + [ - { - "Generated by": "Picasso Render : Mask out", - "Display pixel size [nm]": self.disp_px_size.value(), - "Blur": self.mask_blur.value(), - "Threshold": self.mask_thresh.value(), - } - ] - io.save_locs(out_path, self.index_locs_out[channel], info) class PickToolCircleSettings(QtWidgets.QWidget): """ A class contating information about circular pick. """ From f5927ff8b207fb5616ea27657e578d961cbdd31f Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sun, 8 Dec 2024 23:31:00 +0100 Subject: [PATCH 07/17] AIM undrifting and applying drift from txt yield the same result --- changelog.rst | 16 ++++++---------- picasso/aim.py | 14 ++++++++++---- picasso/gui/render.py | 6 +++--- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/changelog.rst b/changelog.rst index c432a1b..c503d5f 100644 --- a/changelog.rst +++ b/changelog.rst @@ -1,10 +1,13 @@ Changelog ========= -Last change: 06-DEC-2024 MTS +Last change: 09-DEC-2024 MTS -0.7.4 ------ +0.7.1 - 0.7.4 +------------- +- SMLM clusterer in picked regions deleted +- Show legend in Render property displayed rounded tick label values +- Pick circular area does not save the area for each pick in localization's metadata - Picasso: Render's title bar displays the file names of only opened files - Picasso: Render - RESI dialog fixed, units in nm - Picasso: Render - show drift in nm, not camera pixels @@ -12,13 +15,6 @@ Last change: 06-DEC-2024 MTS - CMD localize saves camera information in the metadata file - Other minor bug fixes -0.7.1 - 0.7.3 -------------- -- SMLM clusterer in picked regions deleted -- Show legend in Render property displayed rounded tick label values -- Pick circular area does not save the area for each pick in localization's metadata -- Other minor bug fixes - 0.7.0 ----- - Adaptive Intersection Maximization (AIM, doi: 10.1038/s41592-022-01307-0) implemented diff --git a/picasso/aim.py b/picasso/aim.py index 4336cb2..b0f2217 100644 --- a/picasso/aim.py +++ b/picasso/aim.py @@ -686,9 +686,13 @@ def aim( drift_x = drift_x1 + drift_x2 drift_y = drift_y1 + drift_y2 - # # shift the drifts by the mean value - drift_x -= _np.mean(drift_x) - drift_y -= _np.mean(drift_y) + # shift the drifts by the mean value + shift_x = _np.mean(drift_x) + shift_y = _np.mean(drift_y) + drift_x -= shift_x + drift_y -= shift_y + x_pdc += shift_x + y_pdc += shift_y # combine to Picasso format drift = _np.rec.array((drift_x, drift_y), dtype=[("x", "f"), ("y", "f")]) @@ -713,7 +717,9 @@ def aim( aim_round=2, progress=progress, ) drift_z = drift_z1 + drift_z2 - drift_z -= _np.mean(drift_z) + shift_z = _np.mean(drift_z) + drift_z -= shift_z + z_pdc += shift_z drift = _np.rec.array( (drift_x, drift_y, drift_z), dtype=[("x", "f"), ("y", "f"), ("z", "f")] diff --git a/picasso/gui/render.py b/picasso/gui/render.py index ee4d7d2..0f15193 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -1710,7 +1710,7 @@ class AIMDialog(QtWidgets.QDialog): def __init__(self, window): super().__init__(window) self.window = window - self.setWindowTitle("Enter parameters") + self.setWindowTitle("AIM undrifting") vbox = QtWidgets.QVBoxLayout(self) grid = QtWidgets.QGridLayout() grid.addWidget(QtWidgets.QLabel("Segmentation:"), 0, 0) @@ -9956,7 +9956,7 @@ def apply_drift(self): ) if path: drift = np.loadtxt(path, delimiter=' ') - if hasattr(self.locs[channel], "z"): + if drift.shape[1] == 3: # 3D drift drift = (drift[:,0], drift[:,1], drift[:,2]) drift = np.rec.array( drift, @@ -9980,7 +9980,7 @@ def apply_drift(self): self.locs[channel].z -= drift.z[ self.locs[channel].frame ] - else: + else: # 2D drift drift = (drift[:,0], drift[:,1]) drift = np.rec.array( drift, From 7e3acf9304f0446165008fabdf83f26fac2dcd4a Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sun, 8 Dec 2024 23:47:32 +0100 Subject: [PATCH 08/17] Test clusterer keyboard tracking --- picasso/gui/render.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 0f15193..dfa35ad 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -2426,7 +2426,6 @@ def __init__(self, dialog): grid = QtWidgets.QGridLayout(self) grid.addWidget(QtWidgets.QLabel("Radius (nm):"), 0, 0) self.radius = QtWidgets.QDoubleSpinBox() - self.radius.setKeyboardTracking(False) self.radius.setRange(0.01, 1e6) self.radius.setValue(10) self.radius.setDecimals(2) @@ -2435,7 +2434,6 @@ def __init__(self, dialog): grid.addWidget(QtWidgets.QLabel("Min. samples:"), 1, 0) self.min_samples = QtWidgets.QSpinBox() - self.min_samples.setKeyboardTracking(False) self.min_samples.setValue(4) self.min_samples.setRange(1, int(1e6)) self.min_samples.setSingleStep(1) @@ -2454,7 +2452,6 @@ def __init__(self, dialog): grid = QtWidgets.QGridLayout(self) grid.addWidget(QtWidgets.QLabel("Min. cluster size:"), 0, 0) self.min_cluster_size = QtWidgets.QSpinBox() - self.min_cluster_size.setKeyboardTracking(False) self.min_cluster_size.setValue(10) self.min_cluster_size.setRange(1, int(1e6)) self.min_cluster_size.setSingleStep(1) @@ -2462,7 +2459,6 @@ def __init__(self, dialog): grid.addWidget(QtWidgets.QLabel("Min. samples"), 1, 0) self.min_samples = QtWidgets.QSpinBox() - self.min_samples.setKeyboardTracking(False) self.min_samples.setValue(10) self.min_samples.setRange(1, int(1e6)) self.min_samples.setSingleStep(1) @@ -2472,7 +2468,6 @@ def __init__(self, dialog): QtWidgets.QLabel("Intercluster max.\ndistance (pixels):"), 2, 0 ) self.cluster_eps = QtWidgets.QDoubleSpinBox() - self.cluster_eps.setKeyboardTracking(False) self.cluster_eps.setRange(0, 1e6) self.cluster_eps.setValue(0.0) self.cluster_eps.setDecimals(3) @@ -2492,7 +2487,6 @@ def __init__(self, dialog): grid = QtWidgets.QGridLayout(self) grid.addWidget(QtWidgets.QLabel("Radius xy (nm):"), 0, 0) self.radius_xy = QtWidgets.QDoubleSpinBox() - self.radius_xy.setKeyboardTracking(False) self.radius_xy.setValue(10) self.radius_xy.setRange(0.01, 1e6) self.radius_xy.setSingleStep(0.1) @@ -2501,7 +2495,6 @@ def __init__(self, dialog): grid.addWidget(QtWidgets.QLabel("Radius z (3D only):"), 1, 0) self.radius_z = QtWidgets.QDoubleSpinBox() - self.radius_z.setKeyboardTracking(False) self.radius_z.setValue(25) self.radius_z.setRange(0.01, 1e6) self.radius_z.setSingleStep(0.1) @@ -2510,7 +2503,6 @@ def __init__(self, dialog): grid.addWidget(QtWidgets.QLabel("Min. no. locs"), 2, 0) self.min_locs = QtWidgets.QSpinBox() - self.min_locs.setKeyboardTracking(False) self.min_locs.setValue(10) self.min_locs.setRange(1, int(1e6)) self.min_locs.setSingleStep(1) From 55bd4def0cbd1ba74320c4198e4228721bbf299d Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Mon, 9 Dec 2024 00:00:21 +0100 Subject: [PATCH 09/17] Change function inputs for render gui's (h)dbscan to match the pattern for smlm clusterer --- picasso/gui/render.py | 74 ++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/picasso/gui/render.py b/picasso/gui/render.py index dfa35ad..88df2d3 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -1830,12 +1830,11 @@ def getParams(parent=None): dialog = DbscanDialog(parent) result = dialog.exec_() - return ( - dialog.radius.value(), - dialog.density.value(), - dialog.save_centers.isChecked(), - result == QtWidgets.QDialog.Accepted, - ) + return { + "radius": dialog.radius.value(), + "min_density": dialog.density.value(), + "save_centers": dialog.save_centers.isChecked(), + }, result == QtWidgets.QDialog.Accepted class HdbscanDialog(QtWidgets.QDialog): @@ -1914,13 +1913,12 @@ def getParams(parent=None): dialog = HdbscanDialog(parent) result = dialog.exec_() - return ( - dialog.min_cluster.value(), - dialog.min_samples.value(), - dialog.cluster_eps.value(), - dialog.save_centers.isChecked(), - result == QtWidgets.QDialog.Accepted, - ) + return { + "min_cluster": dialog.min_cluster.value(), + "min_samples": dialog.min_samples.value(), + "cluster_eps": dialog.cluster_eps.value(), + "save_centers": dialog.save_centers.isChecked(), + }, result == QtWidgets.QDialog.Accepted, class LinkDialog(QtWidgets.QDialog): @@ -6027,9 +6025,7 @@ def dbscan(self): channel = self.get_channel_all_seq("DBSCAN") # get DBSCAN parameters - params = DbscanDialog.getParams() - ok = params[-1] # true if parameters were given - + params, ok = DbscanDialog.getParams() if ok: if channel == len(self.locs_paths): # apply to all channels # get saving name suffix @@ -6045,7 +6041,7 @@ def dbscan(self): path = self.locs_paths[channel].replace( ".hdf5", f"{suffix}.hdf5" ) - self._dbscan(channel, path, params) + self._dbscan(channel, path, **params) else: # get the path to save path, ext = QtWidgets.QFileDialog.getSaveFileName( @@ -6055,9 +6051,9 @@ def dbscan(self): filter="*.hdf5", ) if path: - self._dbscan(channel, path, params) + self._dbscan(channel, path, **params) - def _dbscan(self, channel, path, params): + def _dbscan(self, channel, path, radius, min_density, save_centers): """ Performs DBSCAN in a given channel with user-defined parameters and saves the result. @@ -6068,11 +6064,14 @@ def _dbscan(self, channel, path, params): Index of the channel were clustering is performed path : str Path to save clustered localizations - params : list - DBSCAN parameters + radius : float + Radius for DBSCAN clustering in nm + min_density : int + Minimum local density for DBSCAN clustering + save_centers : bool + Specifies if cluster centers should be saved """ - radius, min_density, save_centers, _ = params status = lib.StatusDialog( "Applying DBSCAN. This may take a while.", self ) @@ -6101,7 +6100,6 @@ def _dbscan(self, channel, path, params): "Radius (nm)": radius, "Minimum local density": min_density, } - io.save_locs(path, locs, self.infos[channel] + [dbscan_info]) status.close() if save_centers: @@ -6119,9 +6117,7 @@ def hdbscan(self): channel = self.get_channel_all_seq("HDBSCAN") # get HDBSCAN parameters - params = HdbscanDialog.getParams() - ok = params[-1] # true if parameters were given - + params, ok = HdbscanDialog.getParams() if ok: if channel == len(self.locs_paths): # apply to all channels # get saving name suffix @@ -6137,7 +6133,7 @@ def hdbscan(self): path = self.locs_paths[channel].replace( ".hdf5", f"{suffix}.hdf5" ) - self._hdbscan(channel, path, params) + self._hdbscan(channel, path, **params) else: # get the path to save path, ext = QtWidgets.QFileDialog.getSaveFileName( @@ -6150,9 +6146,17 @@ def hdbscan(self): filter="*.hdf5", ) if path: - self._hdbscan(channel, path, params) + self._hdbscan(channel, path, **params) - def _hdbscan(self, channel, path, params): + def _hdbscan( + self, + channel, + path, + min_cluster, + min_samples, + cluster_eps, + save_centers, + ): """ Performs HDBSCAN in a given channel with user-defined parameters and saves the result. @@ -6163,11 +6167,17 @@ def _hdbscan(self, channel, path, params): Index of the channel were clustering is performed path : str Path to save clustered localizations - params : list - HDBSCAN parameters + min_cluster : int + Minimum number of localizations in a cluster + min_samples : int + Number of localizations within radius to consider a given + point a core sample + cluster_eps : float + Distance threshold. Clusters below this value will be merged + save_centers : bool + Specifies if cluster centers should be saved """ - min_cluster, min_samples, cluster_eps, save_centers, _ = params status = lib.StatusDialog( "Applying HDBSCAN. This may take a while.", self ) From c449bda29da224687dadab6ccdedf18a1b4d3eba Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Mon, 9 Dec 2024 00:34:06 +0100 Subject: [PATCH 10/17] AIM from CMD, remove --mode from CMD undrift --- changelog.rst | 1 + docs/cmd.rst | 4 +++ picasso/__main__.py | 76 +++++++++++++++++++++++++++++++++++++++++---- picasso/aim.py | 4 ++- 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/changelog.rst b/changelog.rst index c503d5f..0f99f1f 100644 --- a/changelog.rst +++ b/changelog.rst @@ -12,6 +12,7 @@ Last change: 09-DEC-2024 MTS - Picasso: Render - RESI dialog fixed, units in nm - Picasso: Render - show drift in nm, not camera pixels - Picasso: Render - masking localizations saves the mask area in metadata +- CMD implementation of AIM undrifting, see ``picasso aim -h`` in terminal - CMD localize saves camera information in the metadata file - Other minor bug fixes diff --git a/docs/cmd.rst b/docs/cmd.rst index 4303efe..ff5d700 100644 --- a/docs/cmd.rst +++ b/docs/cmd.rst @@ -78,6 +78,10 @@ undrift ------- Correct localization coordinates for drift with RCC. +aim +------- +Correct localization coordinates for drift with AIM. + density ------- Compute the local density of localizations diff --git a/picasso/__main__.py b/picasso/__main__.py index fbfee93..e0e435a 100644 --- a/picasso/__main__.py +++ b/picasso/__main__.py @@ -423,6 +423,26 @@ def _undrift(files, segmentation, display=True, fromfile=None): savetxt(base + "_drift.txt", drift, header="dx\tdy", newline="\r\n") +def _undrift_aim( + files, segmentation, intersectdist=20/130, roiradius=60/130 +): + import glob + from . import io, aim + from numpy import savetxt + + paths = glob.glob(files) + for path in paths: + try: + locs, info = io.load_locs(path) + except io.NoMetadataFileError: + continue + print("Undrifting file {}".format(path)) + locs, new_info, drift = aim.aim(locs, info, segmentation, intersectdist, roiradius) + base, ext = os.path.splitext(path) + io.save_locs(base + "_aim.hdf5", locs, new_info) + savetxt(base + "_aimdrift.txt", drift, header="dx\tdy", newline="\r\n") + + def _density(files, radius): import glob @@ -1211,12 +1231,12 @@ def main(): " specified by a unix style path pattern" ), ) - undrift_parser.add_argument( - "-m", - "--mode", - default="render", - help='"std", "render" or "framepair")', - ) + # undrift_parser.add_argument( + # "-m", + # "--mode", + # default="render", + # help='"std", "render" or "framepair")', + # ) undrift_parser.add_argument( "-s", "--segmentation", @@ -1240,6 +1260,48 @@ def main(): help="do not display estimated drift", ) + # undrift by AIM parser + undrift_aim_parser = subparsers.add_parser( + "aim", help="correct localization coordinates for drift with AIM" + ) + undrift_aim_parser.add_argument( + "files", + help=( + "one or multiple hdf5 localization files" + " specified by a unix style path pattern" + ), + ) + undrift_aim_parser.add_argument( + "-s", + "--segmentation", + type=float, + default=100, + help=( + "the number of frames to be combined" + " for one temporal segment (default=100)" + ), + ) + undrift_aim_parser.add_argument( + "-i", + "--intersectdist", + type=float, + default=20/130, + help=( + "max. distance (cam. pixels) between localizations in" + " consecutive segments to be considered as intersecting" + ), + ) + undrift_aim_parser.add_argument( + "-r", + "--roiradius", + type=float, + default=60/130, + help=( + "max. drift (cam. pixels) between two consecutive" + " segments" + ), + ) + # local densitydd density_parser = subparsers.add_parser( "density", help="compute the local density of localizations" @@ -1709,6 +1771,8 @@ def main(): ) elif args.command == "undrift": _undrift(args.files, args.segmentation, args.nodisplay, args.fromfile) + elif args.command == "aim": + _undrift_aim(args.files, args.segmentation, args.intersectdist, args.roiradius) elif args.command == "density": _density(args.files, args.radius) elif args.command == "dbscan": diff --git a/picasso/aim.py b/picasso/aim.py index b0f2217..1010872 100644 --- a/picasso/aim.py +++ b/picasso/aim.py @@ -634,6 +634,8 @@ def aim( ------- locs : _np.rec.array Undrifted localizations. + new_info : list of 1 dict + Updated metadata. drift : _np.rec.array Drift in x and y directions (and z if applicable). """ @@ -732,7 +734,7 @@ def aim( locs["z"] = z_pdc new_info = { - "Undrifted by": "AIM", + "Generated by": "AIM undrift", "Intersect distance (nm)": intersect_d * pixelsize, "Segmentation": segmentation, "Search regions radius (nm)": roi_r * pixelsize, From eb4688b7d3b598ca6bbeddd0c3410bd01f2cc0e2 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Mon, 9 Dec 2024 09:32:56 +0100 Subject: [PATCH 11/17] Render: scale bar is automatically adjusted based on the current FOV's width --- picasso/gui/render.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 88df2d3..35853d0 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -5477,6 +5477,8 @@ class View(QtWidgets.QLabel): Lets user to select picks based on their traces set_mode() Sets self._mode for QMouseEvents + set_optimal_scalebar() + Sets the optimal scalebar length based on the current viewport set_property() Activates rendering by property set_zoom(zoom) @@ -6978,6 +6980,7 @@ def fit_in_view(self, autoscale=False): movie_height, movie_width = self.movie_size() viewport = [(0, 0), (movie_height, movie_width)] self.update_scene(viewport=viewport, autoscale=autoscale) + self.set_optimal_scalebar() def move_to_pick(self): """ Adjust viewport to show a pick identified by its id. """ @@ -7465,6 +7468,7 @@ def mouseReleaseEvent(self, event): y_max = self.viewport[0][0] + y_max_rel * viewport_height viewport = [(y_min, x_min), (y_max, x_max)] self.update_scene(viewport) + self.set_optimal_scalebar() self.rubberband.hide() # stop panning elif event.button() == QtCore.Qt.RightButton: @@ -9572,6 +9576,27 @@ def set_zoom(self, zoom): current_zoom = self.display_pixels_per_viewport_pixels() self.zoom(current_zoom / zoom) + def set_optimal_scalebar(self): + """Sets scalebar to approx. 1/8 of the current viewport's + width""" + + pixelsize = self.window.display_settings_dlg.pixelsize.value() + width = self.viewport_width() + width_nm = width * pixelsize + optimal_scalebar = width_nm / 8 + # approximate to the nearest thousands, hundreds, tens or ones + if optimal_scalebar > 10_000: + scalebar = 10_000 + elif optimal_scalebar > 1_000: + scalebar = int(1_000 * round(optimal_scalebar / 1_000)) + elif optimal_scalebar > 100: + scalebar = int(100 * round(optimal_scalebar / 100)) + elif optimal_scalebar > 10: + scalebar = int(10 * round(optimal_scalebar / 10)) + else: + scalebar = int(round(optimal_scalebar)) + self.window.display_settings_dlg.scalebar.setValue(scalebar) + def sizeHint(self): """ Returns recommended window size. """ @@ -10526,6 +10551,7 @@ def zoom(self, factor, cursor_position=None): ), ] self.update_scene(new_viewport) + self.set_optimal_scalebar() def zoom_in(self): """ Zooms in by a constant factor. """ From bbe1c4e21d835f9dc9b6e4e9cef524319b202c81 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sat, 14 Dec 2024 14:46:28 +0100 Subject: [PATCH 12/17] export channels in grayscale --- changelog.rst | 6 ++- picasso/gui/render.py | 92 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/changelog.rst b/changelog.rst index 0f99f1f..b12cee3 100644 --- a/changelog.rst +++ b/changelog.rst @@ -8,10 +8,12 @@ Last change: 09-DEC-2024 MTS - SMLM clusterer in picked regions deleted - Show legend in Render property displayed rounded tick label values - Pick circular area does not save the area for each pick in localization's metadata -- Picasso: Render's title bar displays the file names of only opened files +- Picasso: Render - adjust the scale bar's size automatically based on the current FOV's width - Picasso: Render - RESI dialog fixed, units in nm - Picasso: Render - show drift in nm, not camera pixels -- Picasso: Render - masking localizations saves the mask area in metadata +- Picasso: Render - masking localizations saves the mask area in its metadata +- Picasso: Render - export current view across channels in grayscale +- Picasso: Render - title bar displays the file only the names of the currently opened files - CMD implementation of AIM undrifting, see ``picasso aim -h`` in terminal - CMD localize saves camera information in the metadata file - Other minor bug fixes diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 35853d0..cf0721a 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -5364,6 +5364,8 @@ class View(QtWidgets.QLabel): Renders sliced locs in the given viewport and draws picks etc dropEvent(event) Defines what happens when a file is dropped onto the window + export_grayscale(suffix) + Renders each channel in grayscale and saves the images. export_trace() Saves trace as a .csv filter_picks() @@ -7027,6 +7029,59 @@ def move_to_pick(self): viewport = [(y_min, x_min), (y_max, x_max)] self.update_scene(viewport=viewport) + def export_grayscale(self, suffix): + """Exports grayscale rendering of the current viewport for each + channel separately.""" + + kwargs = self.get_render_kwargs() + for i, locs in enumerate(self.all_locs): + path = self.locs_paths[i].replace(".hdf5", f"{suffix}.png") + # render like in self.render_single_channel and + # self.render_scene + _, image = render.render(locs, **kwargs, info=self.infos[i]) + image = self.scale_contrast(image) + image = self.to_8bit(image) + cmap = np.uint8( + np.round(255 * plt.get_cmap("gray")(np.arange(256))) + ) + Y, X = image.shape + bgra = np.zeros((Y, X, 4), dtype=np.uint8, order="C") + bgra[:, :, 0] = cmap[:, 2][image] + bgra[:, :, 1] = cmap[:, 1][image] + bgra[:, :, 2] = cmap[:, 0][image] + bgra[:, :, 3] = 255 + qimage = QtGui.QImage(bgra.data, X, Y, QtGui.QImage.Format_RGB32) + # modify qimage like in self.draw_scene + qimage = qimage.scaled( + self.width(), + self.height(), + QtCore.Qt.KeepAspectRatioByExpanding, + ) + qimage = self.draw_scalebar(qimage) + qimage = self.draw_minimap(qimage) + qimage = self.draw_legend(qimage) + qimage = self.draw_picks(qimage) + qimage = self.draw_points(qimage) + # save image + qimage.save(path) + + # save metadata + info = self.window.export_current_info(path=None) + info["Colormap"] = "gray" + io.save_info(path.replace(".png", ".yaml"), [info]) + + # save a copy with scale bar if not present + scalebar = self.window.display_settings_dlg.scalebar_groupbox.isChecked() + if not scalebar: + spath = path.replace(".png", "_scalebar.png") + self.window.display_settings_dlg.scalebar_groupbox.setChecked(True) + qimage_scale = self.draw_scalebar(qimage.copy()) + qimage_scale.save(spath) + self.window.display_settings_dlg.scalebar_groupbox.setChecked(False) + + + + def get_channel(self, title="Choose a channel"): """ Opens an input dialog to ask for a channel. @@ -9030,7 +9085,7 @@ def render_single_channel( """ Renders single channel localizations. - Calls render_multi_channel in case of clustered or picked locs, + Calls render_multi_channel in case of clustered, picked locs or rendering by property) Parameters @@ -10632,6 +10687,8 @@ class Window(QtWidgets.QMainWindow): Exports current view as .png or .tif export_current_info() Exports info about the current view in .yaml file + export_grayscale() + Exports each channel in grayscale. export_multi() Asks the user to choose a type of export export_fov_ims() @@ -10772,6 +10829,10 @@ def initUI(self, plugins_loaded): export_complete_action = file_menu.addAction("Export complete image") export_complete_action.setShortcut("Ctrl+Shift+E") export_complete_action.triggered.connect(self.export_complete) + export_grayscale_action = file_menu.addAction( + "Export channels in grayscale" + ) + export_grayscale_action.triggered.connect(self.export_grayscale) file_menu.addSeparator() export_multi_action = file_menu.addAction("Export localizations") @@ -11083,12 +11144,9 @@ def export_current_info(self, path): ---------- path : str Path for saving the original image with .png or .tif - extension + extension. If None, info is returned and is not saved. """ - path, ext = os.path.splitext(path) - path = path + ".yaml" - fov_info = [ self.info_dialog.change_fov.x_box.value(), self.info_dialog.change_fov.y_box.value(), @@ -11110,10 +11168,15 @@ def export_current_info(self, path): "Colors": colors, "Min. blur (cam. px)": d.min_blur_width.value(), } - io.save_info(path, [info]) + if path is not None: + path, ext = os.path.splitext(path) + path = path + ".yaml" + io.save_info(path, [info]) + else: + return info def export_complete(self): - """ Exports the whole field of view as .png or .tif. """ + """Exports the whole field of view as .png or .tif. """ try: base, ext = os.path.splitext(self.view.locs_paths[0]) @@ -11130,6 +11193,21 @@ def export_complete(self): qimage.save(path) self.export_current_info(path) + def export_grayscale(self): + """Exports each channel in grayscale.""" + + # get the suffix to save the screenshots + + suffix, ok = QtWidgets.QInputDialog.getText( + self, + "Save each channel in grayscale", + "Enter suffix for the screenshots", + QtWidgets.QLineEdit.Normal, + "_grayscale", + ) + if ok: + self.view.export_grayscale(suffix) + def export_txt(self): """ Exports locs as .txt for ImageJ. From a09b8f31697a5f91e9776a6de581e344ba42e7b8 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sat, 14 Dec 2024 16:05:48 +0100 Subject: [PATCH 13/17] FIX automatic scale bar --- picasso/gui/render.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/picasso/gui/render.py b/picasso/gui/render.py index cf0721a..471c118 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -6982,7 +6982,6 @@ def fit_in_view(self, autoscale=False): movie_height, movie_width = self.movie_size() viewport = [(0, 0), (movie_height, movie_width)] self.update_scene(viewport=viewport, autoscale=autoscale) - self.set_optimal_scalebar() def move_to_pick(self): """ Adjust viewport to show a pick identified by its id. """ @@ -7523,7 +7522,6 @@ def mouseReleaseEvent(self, event): y_max = self.viewport[0][0] + y_max_rel * viewport_height viewport = [(y_min, x_min), (y_max, x_max)] self.update_scene(viewport) - self.set_optimal_scalebar() self.rubberband.hide() # stop panning elif event.button() == QtCore.Qt.RightButton: @@ -10421,6 +10419,8 @@ def update_scene( picks_only=picks_only, ) self.update_cursor() + if not use_cache: + self.set_optimal_scalebar() def update_scene_slicer( self, @@ -10440,7 +10440,7 @@ def update_scene_slicer( True if optimally adjust contrast use_cache : boolean (default=False) True if use stored image - cache : boolena (default=True) + cache : boolean (default=True) True if save image """ @@ -10606,7 +10606,6 @@ def zoom(self, factor, cursor_position=None): ), ] self.update_scene(new_viewport) - self.set_optimal_scalebar() def zoom_in(self): """ Zooms in by a constant factor. """ From eb281f661a998448edd524fac6576ab102f93c90 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sat, 14 Dec 2024 16:28:39 +0100 Subject: [PATCH 14/17] Rotation window screenshot saves metadata and extra view with scale bar --- picasso/gui/rotation.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/picasso/gui/rotation.py b/picasso/gui/rotation.py index 4f61c81..61a61ac 100644 --- a/picasso/gui/rotation.py +++ b/picasso/gui/rotation.py @@ -1819,6 +1819,46 @@ def export_current_view(self): ) if path: self.qimage.save(path) + self.export_current_view_info(path) + scalebar = self.window.display_settings_dlg.scalebar_groupbox.isChecked() + if not scalebar: + self.window.display_settings_dlg.scalebar_groupbox.setChecked(True) + self.update_scene() + self.qimage.save(path.replace(".png", "_scalebar.png")) + self.window.display_settings_dlg.scalebar_groupbox.setChecked(False) + self.update_scene() + + def export_current_view_info(self, path): + """ Exports current view's information. """ + + (y_min, x_min), (y_max, x_max) = self.viewport + fov = [x_min, y_min, x_max - x_min, y_max - y_min] + d = self.window.display_settings_dlg + colors = [ + _.currentText() for _ in self.window.dataset_dialog.colorselection + ] + rot_angles = [ + int(self.angx * 180 / np.pi), + int(self.angy * 180 / np.pi), + int(self.angz * 180 / np.pi), + ] + info = { + "Rotation angles (deg)": rot_angles, + "FOV (X, Y, Width, Height)": fov, + "Display pixel size (nm)": d.disp_px_size.value(), + "Min. density": d.minimum.value(), + "Max. density": d.maximum.value(), + "Colormap": d.colormap.currentText(), + "Blur method": d.blur_methods[d.blur_buttongroup.checkedButton()], + "Scalebar length (nm)": d.scalebar.value(), + "Min. blur (cam. px)": d.min_blur_width.value(), + "Localizations loaded": self.paths, + "Colors": colors, + } + path, ext = os.path.splitext(path) + path = path + ".yaml" + io.save_info(path, [info]) + def zoom_in(self): """ Zooms in by a constant factor. """ From ac6f7dbb3d790aeaf5c8018a4d77addc0666cacf Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sat, 14 Dec 2024 16:39:32 +0100 Subject: [PATCH 15/17] automatic scalebar length adjustment for rotation window --- picasso/gui/rotation.py | 136 +++++++++++++++++++++++++++------------- 1 file changed, 93 insertions(+), 43 deletions(-) diff --git a/picasso/gui/rotation.py b/picasso/gui/rotation.py index 61a61ac..39dbcc0 100644 --- a/picasso/gui/rotation.py +++ b/picasso/gui/rotation.py @@ -314,7 +314,7 @@ def silent_maximum_update(self, value): def render_scene(self, *args, **kwargs): """ Updates scene in the rotation window. """ - self.window.view_rot.update_scene() + self.window.view_rot.update_scene(use_cache=True) def set_dynamic_disp_px(self, state): """ Updates scene if dynamic display pixel size is checked. """ @@ -762,6 +762,8 @@ class ViewRotation(QtWidgets.QLabel): Dialog set_mode(action) Sets self._mode for QMouseEvents + set_optimal_scalebar() + Sets the scalebar to approx. 1/8 of the current viewport's width shift_viewport(dx, dy) Moves viewport by dx and dy to_down_rot() @@ -900,6 +902,7 @@ def render_scene( ang=None, animation=False, autoscale=False, + use_cache=False, ): """ Returns QImage with rendered localizations. @@ -915,6 +918,8 @@ def render_scene( If True, scenes are rendered for building an animation autoscale : boolean If True, optimally adjust contrast + use_cache : boolean + If True, use cached image Returns ------- @@ -929,9 +934,13 @@ def render_scene( # render single or multi channel data n_channels = len(self.locs) if n_channels == 1: - self.render_single_channel(kwargs, ang=ang, autoscale=autoscale) + self.render_single_channel( + kwargs, ang=ang, autoscale=autoscale, use_cache=use_cache + ) else: - self.render_multi_channel(kwargs, ang=ang, autoscale=autoscale) + self.render_multi_channel( + kwargs, ang=ang, autoscale=autoscale, use_cache=use_cache + ) # add alpha channel (no transparency) self._bgra[:, :, 3].fill(255) # build QImage @@ -947,6 +956,7 @@ def render_multi_channel( locs=None, ang=None, autoscale=False, + use_cache=False, ): """ Renders and paints multichannel localizations. @@ -965,6 +975,8 @@ def render_multi_channel( angles autoscale : boolean If True, optimally adjust contrast + use_cache : boolean + If True, use cached image Returns ------- @@ -980,24 +992,28 @@ def render_multi_channel( n_channels = len(locs) colors = get_colors(n_channels) # automatic colors - if ang is None: # no build animation - renderings = [ - render.render( - _, **kwargs, - ang=(self.angx, self.angy, self.angz), - ) for _ in locs - ] - else: # build animation - renderings = [ - render.render( - _, **kwargs, - ang=ang, - ) for _ in locs - ] - n_locs = sum([_[0] for _ in renderings]) - image = np.array([_[1] for _ in renderings]) - self.n_locs = n_locs - self.image = image + if use_cache: + n_locs = self.n_locs + image = self.image + else: + if ang is None: # no build animation + renderings = [ + render.render( + _, **kwargs, + ang=(self.angx, self.angy, self.angz), + ) for _ in locs + ] + else: # build animation + renderings = [ + render.render( + _, **kwargs, + ang=ang, + ) for _ in locs + ] + n_locs = sum([_[0] for _ in renderings]) + image = np.array([_[1] for _ in renderings]) + self.n_locs = n_locs + self.image = image # adjust contrast image = self.scale_contrast(image, autoscale=autoscale) @@ -1063,7 +1079,9 @@ def render_multi_channel( self._bgra = self.to_8bit(bgra) # convert to 8 bit return self._bgra - def render_single_channel(self, kwargs, ang=None, autoscale=False): + def render_single_channel( + self, kwargs, ang=None, autoscale=False, use_cache=False + ): """ Renders single channel localizations. @@ -1078,7 +1096,9 @@ def render_single_channel(self, kwargs, ang=None, autoscale=False): Rotation angles to be rendered. If None, takes the current angles autoscale : boolean (default=False) - True if optimally adjust contrast + True if optimally adjust contrast + use_cache : boolean (default=False) + True if the rendered scene should be taken from cache Returns ------- @@ -1098,22 +1118,26 @@ def render_single_channel(self, kwargs, ang=None, autoscale=False): kwargs, locs=locs, ang=ang, autoscale=autoscale ) - if ang is None: # if not build animation - n_locs, image = render.render( - locs, - **kwargs, - info=self.infos[0], - ang=(self.angx, self.angy, self.angz), - ) - else: # if build animation - n_locs, image = render.render( - locs, - **kwargs, - info=self.infos[0], - ang=ang, - ) - self.n_locs = n_locs - self.image = image + if use_cache: + n_locs = self.n_locs + image = self.image + else: + if ang is None: # if not build animation + n_locs, image = render.render( + locs, + **kwargs, + info=self.infos[0], + ang=(self.angx, self.angy, self.angz), + ) + else: # if build animation + n_locs, image = render.render( + locs, + **kwargs, + info=self.infos[0], + ang=ang, + ) + self.n_locs = n_locs + self.image = image # adjust contrast and convert to 8 bits image = self.scale_contrast(image, autoscale=autoscale) @@ -1132,7 +1156,7 @@ def render_single_channel(self, kwargs, ang=None, autoscale=False): self._bgra[..., 2] = cmap[:, 0][image] return self._bgra - def update_scene(self, viewport=None, autoscale=False): + def update_scene(self, viewport=None, autoscale=False, use_cache=False): """ Updates the view of rendered locs. @@ -1142,12 +1166,16 @@ def update_scene(self, viewport=None, autoscale=False): Viewport to be rendered. If None self.viewport is taken autoscale : boolean (default=False) True if optimally adjust contrast + use_cache : boolean (default=False) + True if the rendered scene should be taken from cache """ n_channels = len(self.locs) if n_channels: viewport = viewport or self.viewport - self.draw_scene(viewport, autoscale=autoscale) + self.draw_scene(viewport, autoscale=autoscale, use_cache=use_cache) + if not use_cache: + self.set_optimal_scalebar() # update current position in the animation dialog angx = np.round(self.angx * 180 / np.pi, 1) @@ -1157,7 +1185,7 @@ def update_scene(self, viewport=None, autoscale=False): "{}, {}, {}".format(angx, angy, angz) ) - def draw_scene(self, viewport, autoscale=False): + def draw_scene(self, viewport, autoscale=False, use_cache=False): """ Renders localizations in the given viewport and draws legend, rotation, etc. @@ -1168,12 +1196,14 @@ def draw_scene(self, viewport, autoscale=False): Viewport defining the rendered FOV autoscale : boolean (default=False) True if contrast should be optimally adjusted + use_cache : boolean (default=False) + True if the rendered scene should be taken from cache """ # make sure viewport has the same shape as the main window self.viewport = self.adjust_viewport_to_view(viewport) # render locs - qimage = self.render_scene(autoscale=autoscale) + qimage = self.render_scene(autoscale=autoscale, use_cache=use_cache) # scale image's size to the window self.qimage = qimage.scaled( self.width(), @@ -1592,6 +1622,26 @@ def to_down_rot(self): self.window.move_pick(0, dy) self.shift_viewport(0, dy) + def set_optimal_scalebar(self): + """Sets scalebar to approx. 1/8 of the current viewport's + width""" + + width = self.viewport_width() + width_nm = width * self.pixelsize + optimal_scalebar = width_nm / 8 + # approximate to the nearest thousands, hundreds, tens or ones + if optimal_scalebar > 10_000: + scalebar = 10_000 + elif optimal_scalebar > 1_000: + scalebar = int(1_000 * round(optimal_scalebar / 1_000)) + elif optimal_scalebar > 100: + scalebar = int(100 * round(optimal_scalebar / 100)) + elif optimal_scalebar > 10: + scalebar = int(10 * round(optimal_scalebar / 10)) + else: + scalebar = int(round(optimal_scalebar)) + self.window.display_settings_dlg.scalebar.setValue(scalebar) + def shift_viewport(self, dx, dy): """ Moves viewport by a given amount. From e8a561ab3f2946382dc45559478ee57396a87bf9 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sat, 14 Dec 2024 16:40:52 +0100 Subject: [PATCH 16/17] CLEAN --- changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.rst b/changelog.rst index b12cee3..92401f3 100644 --- a/changelog.rst +++ b/changelog.rst @@ -1,7 +1,7 @@ Changelog ========= -Last change: 09-DEC-2024 MTS +Last change: 14-DEC-2024 MTS 0.7.1 - 0.7.4 ------------- From a3e49612a93e02f323dd0fabf9b539cd0e831a1f Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sat, 14 Dec 2024 16:43:53 +0100 Subject: [PATCH 17/17] =?UTF-8?q?Bump=20version:=200.7.3=20=E2=86=92=200.7?= =?UTF-8?q?.4?= 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 4a32aa0..44b5525 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.7.3 +current_version = 0.7.4 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 b348b3e..9707880 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.3 +AppVersion=0.7.4 DefaultDirName={commonpf}\Picasso DefaultGroupName=Picasso -OutputBaseFilename="Picasso-Windows-64bit-0.7.3" +OutputBaseFilename="Picasso-Windows-64bit-0.7.4" ArchitecturesAllowed=x64 ArchitecturesInstallIn64BitMode=x64 diff --git a/docs/conf.py b/docs/conf.py index 3e5b94d..3c1d694 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.3" +release = "0.7.4" # -- General configuration --------------------------------------------------- diff --git a/picasso/__init__.py b/picasso/__init__.py index 13e48c8..eda0e15 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.3" +__version__ = "0.7.4" _this_file = _ospath.abspath(__file__) _this_dir = _ospath.dirname(_this_file) diff --git a/picasso/__version__.py b/picasso/__version__.py index 996c86f..d1f0035 100644 --- a/picasso/__version__.py +++ b/picasso/__version__.py @@ -1 +1 @@ -VERSION_NO = "0.7.3" +VERSION_NO = "0.7.4" diff --git a/release/one_click_windows_gui/create_installer_windows.bat b/release/one_click_windows_gui/create_installer_windows.bat index f935ae9..4be3233 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.3-py3-none-any.whl" +call pip install "../../dist/picassosr-0.7.4-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 23ba567..6dd6d2d 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.3 +AppVersion=0.7.4 DefaultDirName={commonpf}\Picasso DefaultGroupName=Picasso -OutputBaseFilename="Picasso-Windows-64bit-0.7.3" +OutputBaseFilename="Picasso-Windows-64bit-0.7.4" ArchitecturesAllowed=x64 ArchitecturesInstallIn64BitMode=x64 diff --git a/setup.py b/setup.py index 9e36657..433cf9a 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name="picassosr", - version="0.7.3", + version="0.7.4", 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",