From 622e41fcd5397dc68b77475b9c7c9308751faf65 Mon Sep 17 00:00:00 2001 From: Philippe CARL Date: Tue, 15 Oct 2024 14:12:22 +0000 Subject: [PATCH 01/42] proposal for GUI improvements Within the main updates, both within the Localize and Render tools there are: - the cursor x, y positions as well as their values are displayed within the status bar - the mouse wheel zooms/unzooms centering on the mouse cursor position - it is as well possible to zoom by simply drawing a rectangle - as a left double-clicking is fully unzooming, i.e. similar to a View>Fit_image_to_window or Ctrl+w - and a right click and move able to drag the window in the case it is zoomed (with the open hand mouse cursor) Within the Render tool it is as well possible to set a pick with a Ctrl+Mouse_left_double_click even when being within the zoom mode. As within the Localize tool it is possible to draw a ROI by pressing the cursor key and drawing it. By making a mouse right click inside the ROI and moving it, the ROI will be displaced (with the closed hand mouse cursor) and outside the ROI it is the window that is displaced (with the open hand mouse cursor as described higher). Within the render tool, I added the possibility to enter/leave the 3D slicing mode with the Ctrl+Q shortkeys. Once in the 3D slicing mode, the given slice position (and size) is now indicated within the status_bar_frame_indicator and it is possible to change the slice position with a Ctrl+mouse_wheel. I also changed to "3D slicer menu" the original menu link within which I increased the size of the plot so that it is fully fitting within the window. What I had as well implemented previously and forgot to describe you is that it is possible to add a new pick with a Ctrl+left_mouse_double_click (within the Zoom mode) and erase a given pick with a Ctrl+right_mouse_double_click --- picasso/__main__.py | 42 +++++--- picasso/gausslq.py | 5 +- picasso/gui/localize.py | 230 ++++++++++++++++++++++++++++++++-------- picasso/gui/render.py | 185 +++++++++++++++++++++++++++----- picasso/localize.py | 9 +- picasso/zfit.py | 23 ++-- 6 files changed, 394 insertions(+), 100 deletions(-) mode change 100755 => 100644 picasso/localize.py diff --git a/picasso/__main__.py b/picasso/__main__.py index 6e8327ad..ba901566 100644 --- a/picasso/__main__.py +++ b/picasso/__main__.py @@ -772,7 +772,7 @@ def _localize(args): print("Localize - Parameters:") print("{:<8} {:<15} {:<10}".format("No", "Label", "Value")) - if args.fit_method == "lq-gpu": + if args.fit_method == "lq-gpu" or args.fit_method == "lq-gpu-3d": if gausslq.gpufit_installed: print("GPUfit installed") else: @@ -954,18 +954,34 @@ def prompt_info(): except Exception as e: px = None - localize_info = { - "Generated by": "Picasso Localize", - "ROI": None, #TODO: change if ROI is given - "Box Size": box, - "Min. Net Gradient": min_net_gradient, - "Pixelsize": px, - "Fit method": args.fit_method - } - localize_info.update(camera_info) if args.fit_method == "mle": - localize_info["Convergence Criterion"] = convergence - localize_info["Max. Iterations"] = max_iterations + localize_info = { + "Generated by": "Picasso Localize", + "ROI": None, + "Box Size": box, + "Min. Net Gradient": min_net_gradient, + "Pixelsize": px, + "Fit method": args.fit_method, + "Convergence Criterion": convergence, + "Max. Iterations": max_iterations, + "baseline": args.baseline, + "gain": args.gain, + "qe": args.qe, + "sensitivity": args.sensitivity, + } + else: + localize_info = { + "Generated by": "Picasso Localize", + "ROI": None, + "Box Size": box, + "Min. Net Gradient": min_net_gradient, + "Pixelsize": px, + "Fit method": args.fit_method, + "baseline": args.baseline, + "gain": args.gain, + "qe": args.qe, + "sensitivity": args.sensitivity, + } if args.fit_method == "lq-3d" or args.fit_method == "lq-gpu-3d": print("------------------------------------------") @@ -1004,7 +1020,7 @@ def prompt_info(): if CHECK_DB: print("\n") - print("Assesing quality and adding to DB") + print("Assessing quality and adding to DB") add_file_to_db(path, out_path) print("Done.") print("\n") diff --git a/picasso/gausslq.py b/picasso/gausslq.py index 6a45d163..d2165216 100644 --- a/picasso/gausslq.py +++ b/picasso/gausslq.py @@ -14,7 +14,7 @@ from tqdm import tqdm as _tqdm import numba as _numba import multiprocessing as _multiprocessing -from concurrent import futures as _futures +from concurrent import futures as _futures from . import postprocess as _postprocess try: @@ -81,7 +81,6 @@ def _initial_parameters(spot, size, size_half): def initial_parameters_gpufit(spots, size): - center = (size / 2.0) - 0.5 initial_width = _np.amax([size / 5.0, 1.0]) @@ -155,7 +154,7 @@ def fit_spot(spot): def fit_spots(spots): theta = _np.empty((len(spots), 6), dtype=_np.float32) - theta.fill(_np.nan) + # theta.fill(_np.nan) for i, spot in enumerate(spots): theta[i] = fit_spot(spot) return theta diff --git a/picasso/gui/localize.py b/picasso/gui/localize.py index 8d315845..c4bdb732 100644 --- a/picasso/gui/localize.py +++ b/picasso/gui/localize.py @@ -12,7 +12,9 @@ import os.path import sys import yaml -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5.QtWidgets import QShortcut +from PyQt5.QtGui import QKeySequence import time import numpy as np import traceback @@ -44,7 +46,6 @@ def paintEvent(self, event): rect.setWidth(int(rect.width() - 1)) painter.drawRect(rect) - class View(QtWidgets.QGraphicsView): """The central widget which shows `Scene` objects of individual frames""" @@ -52,11 +53,15 @@ def __init__(self, window): super().__init__(window) self.window = window self.setAcceptDrops(True) + self.setMouseTracking(True) + self.clickInsideRubberBand = False self.pan = False self.hscrollbar = self.horizontalScrollBar() self.vscrollbar = self.verticalScrollBar() self.rubberband = RubberBand(self) self.roi = None + self.roi_point1 = QtCore.QPoint() + self.roi_point2 = QtCore.QPoint() self.numeric_roi = False def mousePressEvent(self, event): @@ -65,26 +70,72 @@ def mousePressEvent(self, event): self.rubberband.setGeometry(QtCore.QRect(self.roi_origin, QtCore.QSize())) self.rubberband.show() elif event.button() == QtCore.Qt.RightButton: + click_pos = event.pos() + rubberband_rect = self.rubberband.geometry() + if self.roi != None and rubberband_rect.contains(click_pos): + self.clickInsideRubberBand = True + self.setCursor(QtCore.Qt.ClosedHandCursor) + else: + self.clickInsideRubberBand = False + self.setCursor(QtCore.Qt.OpenHandCursor) self.pan = True self.pan_start_x = event.x() self.pan_start_y = event.y() - self.setCursor(QtCore.Qt.ClosedHandCursor) event.accept() else: event.ignore() + def mouseDoubleClickEvent(self, event): + self.window.fit_in_view() + def mouseMoveEvent(self, event): + x_on_scene = self.mapToScene(event.x(), event.y()).x() + y_on_scene = self.mapToScene(event.x(), event.y()).y() + if(self.window.movie is not None and x_on_scene > 0 and x_on_scene < np.shape(self.window.movie[self.window.curr_frame_number])[0] - 1): + x_on_screen = str(round(x_on_scene)) + # x_on_screen = str(round(x_on_scene, 2)) + else: + x_on_screen = "-" + if(self.window.movie is not None and y_on_scene > 0 and y_on_scene < np.shape(self.window.movie[self.window.curr_frame_number])[1] - 1): + y_on_screen = str(round(y_on_scene)) + # y_on_screen = str(round(y_on_scene, 2)) + else: + y_on_screen = "-" + if self.window.movie is not None and x_on_screen != "-" and y_on_screen != "-": + value = str(self.window.movie[self.window.curr_frame_number][round(x_on_scene), round(y_on_scene)]) + else: + value = "-" + if value != "-" and x_on_screen != "-" and y_on_screen != "-": + self.window.status_bar.showMessage("x=" + x_on_screen + ", y=" + y_on_screen + ", value=" + value) + else: + self.window.status_bar.showMessage("") + if event.buttons() == QtCore.Qt.LeftButton and not self.numeric_roi: - self.rubberband.setGeometry(QtCore.QRect(self.roi_origin, event.pos())) + if event.x() > self.roi_origin.x(): + self.roi_point1.setX(self.roi_origin.x()) + self.roi_point2.setX(event.x()) + else: + self.roi_point1.setX(event.x()) + self.roi_point2.setX(self.roi_origin.x()) + if event.y() > self.roi_origin.y(): + self.roi_point1.setY(self.roi_origin.y()) + self.roi_point2.setY(event.y()) + else: + self.roi_point1.setY(event.y()) + self.roi_point2.setY(self.roi_origin.y()) + self.rubberband.setGeometry(QtCore.QRect(self.roi_point1, self.roi_point2)) if self.pan: - self.hscrollbar.setValue( - self.hscrollbar.value() - event.x() + self.pan_start_x - ) - self.vscrollbar.setValue( - self.vscrollbar.value() - event.y() + self.pan_start_y - ) - self.pan_start_x = event.x() - self.pan_start_y = event.y() + if self.clickInsideRubberBand: + self.rubberband.move(self.roi_point1.x() + event.x() - self.pan_start_x, self.roi_point1.y() + event.y() - self.pan_start_y) + else: + self.hscrollbar.setValue( + self.hscrollbar.value() - event.x() + self.pan_start_x + ) + self.vscrollbar.setValue( + self.vscrollbar.value() - event.y() + self.pan_start_y + ) + self.pan_start_x = event.x() + self.pan_start_y = event.y() event.accept() else: event.ignore() @@ -99,18 +150,56 @@ def mouseReleaseEvent(self, event): self.rubberband.hide() self.window.parameters_dialog.roi_edit.setText("") else: + if self.roi_end.x() > self.roi_origin.x(): + self.roi_point1.setX(self.roi_origin.x()) + self.roi_point2.setX(self.roi_end.x()) + else: + self.roi_point1.setX(self.roi_end.x()) + self.roi_point2.setX(self.roi_origin.x()) + if self.roi_end.y() > self.roi_origin.y(): + self.roi_point1.setY(self.roi_origin.y()) + self.roi_point2.setY(self.roi_end.y()) + else: + self.roi_point1.setY(self.roi_end.y()) + self.roi_point2.setY(self.roi_origin.y()) + modifiers = QtWidgets.QApplication.keyboardModifiers() + if modifiers == QtCore.Qt.ControlModifier: + roi_points = ( + self.mapToScene(self.roi_point1), + self.mapToScene(self.roi_point2) + ) + self.roi = [[int(_.y()), int(_.x())] for _ in roi_points] + (y_min, x_min), (y_max, x_max) = self.roi + self.window.parameters_dialog.roi_edit.setText( + f"{y_min},{x_min},{y_max},{x_max}" + ) + self.numeric_roi = False + else: + self.rubberband.hide() + self.fitInView( + self.mapToScene(self.roi_point1).x(), + self.mapToScene(self.roi_point1).y(), + self.mapToScene(self.roi_point2).x() - self.mapToScene(self.roi_point1).x(), + self.mapToScene(self.roi_point2).y() - self.mapToScene(self.roi_point1).y(), + QtCore.Qt.KeepAspectRatio) + self.window.draw_frame() + elif event.button() == QtCore.Qt.RightButton: + # if self.clickInsideRubberBand: + if self.roi != None: + rubberband_rect = self.rubberband.geometry() + self.roi_point1.setX(rubberband_rect.x()) + self.roi_point2.setX(rubberband_rect.x() + rubberband_rect.width()) + self.roi_point1.setY(rubberband_rect.y()) + self.roi_point2.setY(rubberband_rect.y() + rubberband_rect.height()) roi_points = ( - self.mapToScene(self.roi_origin), - self.mapToScene(self.roi_end), + self.mapToScene(self.roi_point1), + self.mapToScene(self.roi_point2) ) self.roi = [[int(_.y()), int(_.x())] for _ in roi_points] (y_min, x_min), (y_max, x_max) = self.roi self.window.parameters_dialog.roi_edit.setText( f"{y_min},{x_min},{y_max},{x_max}" ) - self.numeric_roi = False - self.window.draw_frame() - elif event.button() == QtCore.Qt.RightButton: self.pan = False self.setCursor(QtCore.Qt.ArrowCursor) event.accept() @@ -118,10 +207,24 @@ def mouseReleaseEvent(self, event): event.ignore() def wheelEvent(self, event): - """Implements zoooming with the mouse wheel""" - scale = 1.008 ** (-event.angleDelta().y()) - self.scale(scale, scale) - + modifiers = QtWidgets.QApplication.keyboardModifiers() + if modifiers == QtCore.Qt.ControlModifier: + if self.window.movie is not None: + step = int(event.angleDelta().y() / abs(event.angleDelta().y())) + if self.window.curr_frame_number + step >= 0 and self.window.curr_frame_number + step < self.window.info[0]["Frames"]: + self.window.set_frame(self.window.curr_frame_number + step) + elif modifiers == (QtCore.Qt.ShiftModifier | QtCore.Qt.ControlModifier): + if self.window.movie is not None: + step = 10 * int(event.angleDelta().y() / abs(event.angleDelta().y())) + if self.window.curr_frame_number + step >= 0 and self.window.curr_frame_number + step < self.window.info[0]["Frames"]: + self.window.set_frame(self.window.curr_frame_number + step) + else: + """Implements zoooming with the mouse wheel""" + pos_on_scene = self.mapToScene(event.pos()) + self.centerOn(pos_on_scene) + # scale = 1.008 ** (event.angleDelta().y()) + scale = 1.00153 ** event.angleDelta().y() + self.scale(scale, scale) class Scene(QtWidgets.QGraphicsScene): """ @@ -579,11 +682,10 @@ def __init__(self, parent=None): # Baseline photon_grid.addWidget(QtWidgets.QLabel("Baseline:"), 1, 0) - self.baseline = QtWidgets.QDoubleSpinBox() - self.baseline.setRange(0, 1e6) - self.baseline.setValue(100.0) - self.baseline.setDecimals(1) - self.baseline.setSingleStep(0.1) + self.baseline = QtWidgets.QSpinBox() + self.baseline.setRange(0, int(1e6)) + self.baseline.setValue(100) + self.baseline.setSingleStep(1) photon_grid.addWidget(self.baseline, 1, 1) # Sensitivity @@ -777,7 +879,7 @@ def reset_quality_check(self): def on_roi_edit_changed(self): if self.roi_edit.text() == "": - self.window.view.numeric_roi = False + self.window.view.numeric_roi = False self.window.view.roi = None self.window.view.rubberband.hide() self.window.draw_frame() @@ -797,7 +899,8 @@ def on_roi_edit_finished(self): ) self.window.view.rubberband.show() self.window.draw_frame() - self.window.view.numeric_roi = True + # self.window.view.numeric_roi = True + self.window.view.numeric_roi = False def on_fit_method_changed(self, state): if self.fit_method.currentText() == "LQ, Gaussian": @@ -1156,10 +1259,27 @@ def init_menu_bar(self): previous_frame_action.setShortcut("Left") previous_frame_action.triggered.connect(self.previous_frame) view_menu.addAction(previous_frame_action) - next_frame_action = view_menu.addAction("Next frame") + next_frame_action = view_menu.addAction("Next frame") + # next_frame_action.setShortcuts(["Up", "Right"]) next_frame_action.setShortcut("Right") next_frame_action.triggered.connect(self.next_frame) view_menu.addAction(next_frame_action) + shortcut_previous_frame_10 = QShortcut(QKeySequence("Shift+Left"), self) + shortcut_previous_frame_10.activated.connect(self.previous_frame_10) + shortcut_next_frame_10 = QShortcut(QKeySequence("Shift+Right"), self) + shortcut_next_frame_10.activated.connect(self.next_frame_10) + previous_frame_25_action = view_menu.addAction("Previous frame 25") + previous_frame_25_action.setShortcut("Down") + previous_frame_25_action.triggered.connect(self.previous_frame_25) + view_menu.addAction(previous_frame_25_action) + next_frame_action_25 = view_menu.addAction("Next frame 25") + next_frame_action_25.setShortcut("Up") + next_frame_action_25.triggered.connect(self.next_frame_25) + view_menu.addAction(next_frame_action_25) + shortcut_previous_frame_50 = QShortcut(QKeySequence("Shift+Down"), self) + shortcut_previous_frame_50.activated.connect(self.previous_frame_50) + shortcut_next_frame_50 = QShortcut(QKeySequence("Shift+Up"), self) + shortcut_next_frame_50.activated.connect(self.next_frame_50) view_menu.addSeparator() first_frame_action = view_menu.addAction("First frame") first_frame_action.setShortcut("Home") @@ -1221,10 +1341,10 @@ def init_menu_bar(self): @property def camera_info(self): camera_info = {} - camera_info["Baseline"] = self.parameters_dialog.baseline.value() - camera_info["Gain"] = self.parameters_dialog.gain.value() - camera_info["Sensitivity"] = self.parameters_dialog.sensitivity.value() - camera_info["Qe"] = self.parameters_dialog.qe.value() + camera_info["baseline"] = self.parameters_dialog.baseline.value() + camera_info["gain"] = self.parameters_dialog.gain.value() + camera_info["sensitivity"] = self.parameters_dialog.sensitivity.value() + camera_info["qe"] = self.parameters_dialog.qe.value() return camera_info def calibrate_z(self): @@ -1281,7 +1401,8 @@ def open(self, path): ) self.setWindowTitle( - "Picasso: Localize. File: {}".format(os.path.basename(path)) + # "Picasso: Localize. File: {}".format(os.path.basename(path)) + "Picasso: Localize. File: {}".format(path) ) self.parameters_dialog.reset_quality_check() @@ -1474,14 +1595,38 @@ def prompt_channel(self, channels): return channel def previous_frame(self): - if self.movie is not None: - if self.curr_frame_number > 0: - self.set_frame(self.curr_frame_number - 1) + self.previous_frame_step(1) def next_frame(self): + self.next_frame_step(1) + + def previous_frame_10(self): + self.previous_frame_step(10) + + def next_frame_10(self): + self.next_frame_step(10) + + def previous_frame_25(self): + self.previous_frame_step(25) + + def next_frame_25(self): + self.next_frame_step(25) + + def previous_frame_50(self): + self.previous_frame_step(50) + + def next_frame_50(self): + self.next_frame_step(50) + + def previous_frame_step(self, step): + if self.movie is not None: + if self.curr_frame_number - step >= 0: + self.set_frame(self.curr_frame_number - step) + + def next_frame_step(self, step): if self.movie is not None: - if self.curr_frame_number + 1 < self.info[0]["Frames"]: - self.set_frame(self.curr_frame_number + 1) + if self.curr_frame_number + step < self.info[0]["Frames"]: + self.set_frame(self.curr_frame_number + step) def first_frame(self): if self.movie is not None: @@ -1732,6 +1877,7 @@ def on_fit_finished(self, locs, elapsed_time, fit_z, calibrate_z): zfit.calibrate_z( locs, self.info, + self.last_identification_info["Min. Net Gradient"], step, self.parameters_dialog.magnification_factor.value(), path=path, @@ -1924,7 +2070,7 @@ def run(self): if self.use_gpufit: self.progressMade.emit(1, 1) theta = gausslq.fit_spots_gpufit(spots) - em = self.camera_info["Gain"] > 1 + em = self.camera_info["gain"] > 1 locs = gausslq.locs_from_fits_gpufit( self.identifications, theta, self.box, em ) @@ -1937,7 +2083,7 @@ def run(self): ) time.sleep(0.2) theta = gausslq.fits_from_futures(fs) - em = self.camera_info["Gain"] > 1 + em = self.camera_info["gain"] > 1 locs = gausslq.locs_from_fits(self.identifications, theta, self.box, em) elif self.method == "mle": curr, thetas, CRLBs, llhoods, iterations = gaussmle.gaussmle_async( @@ -1963,7 +2109,7 @@ def run(self): self.progressMade.emit(round(N * lib.n_futures_done(fs) / n_tasks), N) time.sleep(0.2) theta = avgroi.fits_from_futures(fs) - em = self.camera_info["Gain"] > 1 + em = self.camera_info["gain"] > 1 locs = avgroi.locs_from_fits(self.identifications, theta, self.box, em) else: print("This should never happen...") diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 6ebd49ea..e9df3587 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -21,6 +21,7 @@ import matplotlib.pyplot as plt import matplotlib.patches as patches import numpy as np +import pandas as pd import yaml from matplotlib.backends.backend_qt5agg import FigureCanvas @@ -3892,7 +3893,8 @@ def __init__(self, window, tools_settings_dialog): self.grid.addWidget(QtWidgets.QLabel("Diameter (cam. pixel):"), 0, 0) self.pick_diameter = QtWidgets.QDoubleSpinBox() self.pick_diameter.setRange(0.001, 999999) - self.pick_diameter.setValue(1) + # self.pick_diameter.setValue(1) + self.pick_diameter.setValue(10) self.pick_diameter.setSingleStep(0.1) self.pick_diameter.setDecimals(3) self.pick_diameter.setKeyboardTracking(False) @@ -4481,7 +4483,8 @@ def __init__(self, window): self.minimum.setRange(0, 999999) self.minimum.setSingleStep(5) self.minimum.setValue(0) - self.minimum.setDecimals(6) + # self.minimum.setDecimals(6) + self.minimum.setDecimals(0) self.minimum.setKeyboardTracking(False) self.minimum.valueChanged.connect(self.update_scene) contrast_grid.addWidget(self.minimum, 0, 1) @@ -4490,8 +4493,10 @@ def __init__(self, window): self.maximum = QtWidgets.QDoubleSpinBox() self.maximum.setRange(0, 999999) self.maximum.setSingleStep(5) - self.maximum.setValue(100) - self.maximum.setDecimals(6) + self.maximum.setValue(20) + # self.maximum.setValue(100) + # self.maximum.setDecimals(6) + self.maximum.setDecimals(0) self.maximum.setKeyboardTracking(False) self.maximum.valueChanged.connect(self.update_scene) contrast_grid.addWidget(self.maximum, 1, 1) @@ -4981,7 +4986,7 @@ def __init__(self, window): self.sl.valueChanged.connect(self.on_slice_position_changed) slicer_grid.addWidget(self.sl, 1, 0, 1, 2) - self.figure, self.ax = plt.subplots(1, figsize=(3, 3)) + self.figure, self.ax = plt.subplots(1, figsize=(7, 7)) self.canvas = FigureCanvas(self.figure) slicer_grid.addWidget(self.canvas, 2, 0, 1, 2) @@ -5007,6 +5012,7 @@ def initialize(self): """ self.calculate_histogram() + self.window.status_bar_frame_indicator.setText("") self.show() def calculate_histogram(self): @@ -5053,8 +5059,8 @@ def calculate_histogram(self): self.ax.set_ylabel("Rel. frequency") self.ax.set_title(r"$\mathrm{Histogram\ of\ Z:}$") self.canvas.draw() - self.sl.setMaximum(int(len(self.bins)) - 2) - self.sl.setValue(int(len(self.bins) / 2)) + self.sl.setMaximum(int(len(self.bins) - 2)) + self.sl.setValue (int(len(self.bins) / 2)) # reset cache self.slicer_cache = {} @@ -5071,11 +5077,25 @@ def on_pick_slice_changed(self): self.sl.setValue(int(len(self.bins) / 2)) # self.on_slice_position_changed(self.sl.value()) + def toggle_slicer_and_radio_button_check(self): + if not hasattr(SlicerDialog, "slicermin"): + self.calculate_histogram() + self.window.slicer_dialog.slicer_radio_button.setChecked(not self.window.slicer_dialog.slicer_radio_button.checkState()) + self.toggle_slicer + + def toggle_slicer(self): """ Updates scene in the main window slicing is called. """ self.window.view.update_scene() + if not self.window.slicer_dialog.slicer_radio_button.isChecked(): + self.window.status_bar_frame_indicator.setText("") + else: + self.window.status_bar_frame_indicator.setText( + "{:,}/{:,}".format(self.window.slicer_dialog.sl.value(), self.window.slicer_dialog.sl.maximum()) + ) + def on_slice_position_changed(self, position): """ Changes some properties and updates scene in the main window. @@ -5092,6 +5112,10 @@ def on_slice_position_changed(self, position): self.slicermax = self.bins[position + 1] self.window.view.update_scene_slicer() + self.window.status_bar_frame_indicator.setText( + "{:,}/{:,}".format(self.window.slicer_dialog.sl.value(), self.window.slicer_dialog.sl.maximum()) + ) + def export_stack(self): """ Saves all slices as .tif files. """ @@ -5554,6 +5578,7 @@ def __init__(self, window): icon = QtGui.QIcon(icon_path) self.icon = icon self.setAcceptDrops(True) + self.setMouseTracking(True) self.setSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding ) @@ -5561,6 +5586,8 @@ def __init__(self, window): QtWidgets.QRubberBand.Rectangle, self ) self.rubberband.setStyleSheet("selection-background-color: white") + self.roi_point1 = QtCore.QPoint() + self.roi_point2 = QtCore.QPoint() self.window = window self._pixmap = None self.all_locs = [] # for fast render @@ -5705,7 +5732,8 @@ def add(self, path, render=True): self.window.setWindowTitle( "Picasso v{}: Render. File: {}".format( - __version__, os.path.basename(path) + # __version__, os.path.basename(path) + __version__, path ) ) @@ -5737,7 +5765,8 @@ def add_multiple(self, paths): pd.set_value(i+1) if len(self.locs): # if loading was successful if fit_in_view: - self.fit_in_view(autoscale=True) + # self.fit_in_view(autoscale=True) + self.fit_in_view(autoscale=False) else: self.update_scene() @@ -7334,6 +7363,17 @@ def max_movie_width(self): return max([info[0]["Width"] for info in self.infos]) + # Function to find the nearest (x, y) and return the corresponding photons value + def find_nearest_photons(self, df, input_x, input_y): + # Calculate the Euclidean distance from the input (x, y) to all points + df['distance'] = np.sqrt((df['x'] - input_x)**2 + (df['y'] - input_y)**2) + + # Find the index of the row with the minimum distance + nearest_index = df['distance'].idxmin() + + # Return the corresponding photons value + return df.loc[nearest_index, 'photons'] + def mouseMoveEvent(self, event): """ Defines actions taken when moving mouse. @@ -7346,12 +7386,32 @@ def mouseMoveEvent(self, event): event : QMouseEvent """ + if hasattr(self, 'viewport'): + x_on_scene, y_on_scene = self.map_to_movie(event.pos()) + + df = pd.DataFrame(self.all_locs[0]) + + # nearest_photons = self.find_nearest_photons(df, x_on_scene, y_on_scene) + # self.window.status_bar.showMessage("x=" + str(round(x_on_scene, 2)) + ", y=" + str(round(y_on_scene, 2)) + ", value=" + str(nearest_photons)) + + self.window.status_bar.showMessage("x=" + str(round(x_on_scene, 2)) + ", y=" + str(round(y_on_scene, 2)) + ", value=" + str(self.find_nearest_photons(df, x_on_scene, y_on_scene))) + if self._mode == "Zoom": # if zooming in if self.rubberband.isVisible(): - self.rubberband.setGeometry( - QtCore.QRect(self.origin, event.pos()) - ) + if event.x() > self.origin.x(): + self.roi_point1.setX(self.origin.x()) + self.roi_point2.setX(event.x()) + else: + self.roi_point1.setX(event.x()) + self.roi_point2.setX(self.origin.x()) + if event.y() > self.origin.y(): + self.roi_point1.setY(self.origin.y()) + self.roi_point2.setY(event.y()) + else: + self.roi_point1.setY(event.y()) + self.roi_point2.setY(self.origin.y()) + self.rubberband.setGeometry(QtCore.QRect(self.roi_point1, self.roi_point2)) # if panning if self._pan: rel_x_move = (event.x() - self.pan_start_x) / self.width() @@ -7359,6 +7419,7 @@ def mouseMoveEvent(self, event): self.pan_relative(rel_y_move, rel_x_move) self.pan_start_x = event.x() self.pan_start_y = event.y() + self.setCursor(QtCore.Qt.OpenHandCursor) # if drawing a rectangular pick elif self._mode == "Pick": if self._pick_shape == "Rectangle": @@ -7394,7 +7455,7 @@ def mousePressEvent(self, event): self._pan = True self.pan_start_x = event.x() self.pan_start_y = event.y() - self.setCursor(QtCore.Qt.ClosedHandCursor) + self.setCursor(QtCore.Qt.OpenHandCursor) event.accept() else: event.ignore() @@ -7407,6 +7468,50 @@ def mousePressEvent(self, event): self.rectangle_pick_start_y = event.y() self.rectangle_pick_start = self.map_to_movie(event.pos()) + def mouseDoubleClickEvent(self, event): + modifiers = QtWidgets.QApplication.keyboardModifiers() + if modifiers == QtCore.Qt.ControlModifier: + if self._pick_shape == "Circle": + # add pick + if event.button() == QtCore.Qt.LeftButton: + x, y = self.map_to_movie(event.pos()) + self.add_pick((x, y)) + event.accept() + # remove pick + elif event.button() == QtCore.Qt.RightButton: + x, y = self.map_to_movie(event.pos()) + self.remove_picks((x, y)) + event.accept() + else: + event.ignore() + elif self._pick_shape == "Rectangle": + if event.button() == QtCore.Qt.LeftButton: + # finish drawing rectangular pick and add it + rectangle_pick_end = self.map_to_movie(event.pos()) + self._rectangle_pick_ongoing = False + self.add_pick( + (self.rectangle_pick_start, rectangle_pick_end) + ) + event.accept() + elif event.button() == QtCore.Qt.RightButton: + # remove pick + x, y = self.map_to_movie(event.pos()) + self.remove_picks((x, y)) + event.accept() + else: + event.ignore() + elif self._pick_shape == "Polygon": + # add a point to the polygon + if event.button() == QtCore.Qt.LeftButton: + point_movie = self.map_to_movie(event.pos()) + self.add_polygon_point(point_movie, event.pos()) + # remove the last point from the polygon + elif event.button() == QtCore.Qt.RightButton: + self.remove_polygon_point() + else: + # elif self._mode == "Zoom": + self.fit_in_view() + def mouseReleaseEvent(self, event): """ Defines actions taken when releasing mouse button. @@ -7425,11 +7530,20 @@ def mouseReleaseEvent(self, event): and self.rubberband.isVisible() ): # zoom in if the zoom-in rectangle is visible end = QtCore.QPoint(event.pos()) - if end.x() > self.origin.x() and end.y() > self.origin.y(): + x_min_rel = x_max_rel = y_min_rel = y_max_rel = 0 + if end.x() > self.origin.x(): x_min_rel = self.origin.x() / self.width() - x_max_rel = end.x() / self.width() + x_max_rel = end.x() / self.width() + if self.origin.x() > end.x(): + x_min_rel = end.x() / self.width() + x_max_rel = self.origin.x() / self.width() + if end.y() > self.origin.y(): y_min_rel = self.origin.y() / self.height() - y_max_rel = end.y() / self.height() + y_max_rel = end.y() / self.height() + if self.origin.y() > end.y(): + y_min_rel = end.y() / self.height() + y_max_rel = self.origin.y() / self.height() + if x_min_rel != 0 and x_max_rel != 0 and y_min_rel != 0 and y_max_rel != 0: viewport_height, viewport_width = self.viewport_size() x_min = self.viewport[0][1] + x_min_rel * viewport_width x_max = self.viewport[0][1] + x_max_rel * viewport_width @@ -10505,21 +10619,30 @@ def zoom_out(self): self.zoom(ZOOM) - def wheelEvent(self, QWheelEvent): + def wheelEvent(self, event): """ Defines what happens when mouse wheel is used. Press Ctrl/Command to zoom in/out. """ - - modifiers = QtWidgets.QApplication.keyboardModifiers() - if modifiers == QtCore.Qt.ControlModifier: - direction = QWheelEvent.angleDelta().y() - position = self.map_to_movie(QWheelEvent.pos()) - if direction > 0: - self.zoom(1 / ZOOM, cursor_position=position) + + if hasattr(self, 'viewport'): + direction = event.angleDelta().y() + modifiers = QtWidgets.QApplication.keyboardModifiers() + if modifiers == QtCore.Qt.ControlModifier: + if hasattr(self.window.slicer_dialog, 'patches'): + if direction < 0 and self.window.slicer_dialog.sl.value() > 0: + self.window.slicer_dialog.sl.setValue(self.window.slicer_dialog.sl.value() - 1) + self.window.slicer_dialog.on_slice_position_changed(self.window.slicer_dialog.sl.value()) + elif direction > 0 and self.window.slicer_dialog.sl.value() < self.window.slicer_dialog.sl.maximum(): + self.window.slicer_dialog.sl.setValue(self.window.slicer_dialog.sl.value() + 1) + self.window.slicer_dialog.on_slice_position_changed(self.window.slicer_dialog.sl.value()) else: - self.zoom(ZOOM, cursor_position=position) + position = self.map_to_movie(event.pos()) + if direction > 0: + self.zoom(1 / ZOOM, cursor_position=position) + else: + self.zoom(ZOOM, cursor_position=position) class Window(QtWidgets.QMainWindow): @@ -10648,6 +10771,9 @@ def initUI(self, plugins_loaded): self.view = View(self) # displays rendered locs self.view.setMinimumSize(1, 1) self.setCentralWidget(self.view) + self.status_bar = self.statusBar() + self.status_bar_frame_indicator = QtWidgets.QLabel() + self.status_bar.addPermanentWidget(self.status_bar_frame_indicator) # set up dialogs self.display_settings_dlg = DisplaySettingsDialog(self) @@ -10771,8 +10897,13 @@ def initUI(self, plugins_loaded): info_action.setShortcut("Ctrl+I") info_action.triggered.connect(self.info_dialog.show) view_menu.addAction(info_action) - slicer_action = view_menu.addAction("Slice") + slicer_action = view_menu.addAction("3D slicer menu") slicer_action.triggered.connect(self.slicer_dialog.initialize) + slicing_action = view_menu.addAction("3D slicing") + # slicing_action.triggered.connect(self.slicer_dialog.initialize) + # slicing_action.triggered.connect(self.view.update_scene) + slicing_action.triggered.connect(self.slicer_dialog.toggle_slicer_and_radio_button_check) + slicing_action.setShortcut("Ctrl+Q") rot_win_action = view_menu.addAction("Update rotation window") rot_win_action.setShortcut("Ctrl+Shift+R") rot_win_action.triggered.connect(self.rot_win) @@ -10938,7 +11069,7 @@ def initUI(self, plugins_loaded): resi_action = postprocess_menu.addAction("RESI") resi_action.triggered.connect(self.open_resi_dialog) - self.load_user_settings() + self.load_user_settings() # Define 3D entries self.actions_3d = [ diff --git a/picasso/localize.py b/picasso/localize.py old mode 100755 new mode 100644 index 48065967..93f4dd7b --- a/picasso/localize.py +++ b/picasso/localize.py @@ -344,7 +344,6 @@ def _cut_spots(movie, ids, box): return spots else: """Assumes that identifications are in order of frames!""" - r = int(box / 2) N = len(ids.frame) spots = _np.zeros((N, box, box), dtype=movie.dtype) @@ -355,11 +354,11 @@ def _cut_spots(movie, ids, box): def _to_photons(spots, camera_info): spots = _np.float32(spots) - baseline = camera_info["Baseline"] - sensitivity = camera_info["Sensitivity"] - gain = camera_info["Gain"] + baseline = camera_info["baseline"] + sensitivity = camera_info["sensitivity"] + gain = camera_info["gain"] # since v0.6.0: remove quantum efficiency to better reflect precision - # qe = camera_info["Qe"] + # qe = camera_info["qe"] return (spots - baseline) * sensitivity / (gain) diff --git a/picasso/zfit.py b/picasso/zfit.py index 9bc39da5..13919b9b 100644 --- a/picasso/zfit.py +++ b/picasso/zfit.py @@ -23,11 +23,11 @@ def interpolate_nan(data): return data -def calibrate_z(locs, info, d, magnification_factor, path=None): +def calibrate_z(locs, info, min_net_gradient, step, magnification_factor, path=None): n_frames = info[0]["Frames"] - range = (n_frames - 1) * d + range = (n_frames - 1) * step frame_range = _np.arange(n_frames) - z_range = -(frame_range * d - range / 2) # negative so that the first frames of + z_range = -(frame_range * step - range / 2) # negative so that the first frames of # a bottom-to-up scan are positive z coordinates. mean_sx = _np.array([_np.mean(locs.sx[locs.frame == _]) for _ in frame_range]) @@ -52,20 +52,21 @@ def calibrate_z(locs, info, d, magnification_factor, path=None): cy = _np.polyfit(z_range, mean_sy, 6, full=False) # Fits calibration curve to each localization - # true_z = locs.frame * d - range / 2 + # true_z = locs.frame * step - range / 2 # cx = _np.polyfit(true_z, locs.sx, 6, full=False) # cy = _np.polyfit(true_z, locs.sy, 6, full=False) calibration = { - "X Coefficients": [float(_) for _ in cx], - "Y Coefficients": [float(_) for _ in cy], - "Number of frames": int(n_frames), - "Step size in nm": float(d), + "Calibration min. net gradient": int(min_net_gradient), "Magnification factor": float(magnification_factor), + "Number of frames": int(n_frames), + "Step size in nm": float(step), + "X Coefficients": [float(_) for _ in cx], + "Y Coefficients": [float(_) for _ in cy] } if path is not None: with open(path, "w") as f: - _yaml.dump(calibration, f, default_flow_style=False) + _yaml.dump(calibration, f, sort_keys=False, default_flow_style=False) locs = fit_z(locs, info, calibration, magnification_factor) locs.z /= magnification_factor @@ -251,6 +252,7 @@ def fit_z_parallel( int(n_locs / n_tasks + 1) if _ < n_locs % n_tasks else int(n_locs / n_tasks) for _ in range(n_tasks) ] + start_indices = _np.cumsum([0] + spots_per_task[:-1]) fs = [] executor = _ProcessPoolExecutor(n_workers) @@ -267,7 +269,8 @@ def fit_z_parallel( ) if asynch: return fs - with _tqdm(total=n_tasks, unit="task") as progress_bar: +# with _tqdm( total=n_tasks, unit="task") as progress_bar: + with _tqdm(desc="3D Fitting", total=n_tasks, unit="task") as progress_bar: for f in _futures.as_completed(fs): progress_bar.update() return locs_from_futures(fs, filter=filter) From 1b5814ba3a2f0a73266e18e762a3d8154f1ff40f Mon Sep 17 00:00:00 2001 From: Philippe CARL Date: Thu, 2 Jan 2025 19:23:20 +0100 Subject: [PATCH 02/42] Update zfit.py I erased the changes in this file besides the tqdm description --- picasso/zfit.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/picasso/zfit.py b/picasso/zfit.py index 13919b9b..099fafde 100644 --- a/picasso/zfit.py +++ b/picasso/zfit.py @@ -23,11 +23,11 @@ def interpolate_nan(data): return data -def calibrate_z(locs, info, min_net_gradient, step, magnification_factor, path=None): +def calibrate_z(locs, info, d, magnification_factor, path=None): n_frames = info[0]["Frames"] - range = (n_frames - 1) * step + range = (n_frames - 1) * d frame_range = _np.arange(n_frames) - z_range = -(frame_range * step - range / 2) # negative so that the first frames of + z_range = -(frame_range * d - range / 2) # negative so that the first frames of # a bottom-to-up scan are positive z coordinates. mean_sx = _np.array([_np.mean(locs.sx[locs.frame == _]) for _ in frame_range]) @@ -52,21 +52,21 @@ def calibrate_z(locs, info, min_net_gradient, step, magnification_factor, path=N cy = _np.polyfit(z_range, mean_sy, 6, full=False) # Fits calibration curve to each localization - # true_z = locs.frame * step - range / 2 + # true_z = locs.frame * d - range / 2 # cx = _np.polyfit(true_z, locs.sx, 6, full=False) # cy = _np.polyfit(true_z, locs.sy, 6, full=False) calibration = { - "Calibration min. net gradient": int(min_net_gradient), - "Magnification factor": float(magnification_factor), - "Number of frames": int(n_frames), - "Step size in nm": float(step), "X Coefficients": [float(_) for _ in cx], - "Y Coefficients": [float(_) for _ in cy] + "Y Coefficients": [float(_) for _ in cy], + "Number of frames": int(n_frames), + "Step size in nm": float(d), + "Magnification factor": float(magnification_factor), + } if path is not None: with open(path, "w") as f: - _yaml.dump(calibration, f, sort_keys=False, default_flow_style=False) + _yaml.dump(calibration, f, default_flow_style=False) locs = fit_z(locs, info, calibration, magnification_factor) locs.z /= magnification_factor @@ -252,7 +252,6 @@ def fit_z_parallel( int(n_locs / n_tasks + 1) if _ < n_locs % n_tasks else int(n_locs / n_tasks) for _ in range(n_tasks) ] - start_indices = _np.cumsum([0] + spots_per_task[:-1]) fs = [] executor = _ProcessPoolExecutor(n_workers) @@ -269,7 +268,6 @@ def fit_z_parallel( ) if asynch: return fs -# with _tqdm( total=n_tasks, unit="task") as progress_bar: with _tqdm(desc="3D Fitting", total=n_tasks, unit="task") as progress_bar: for f in _futures.as_completed(fs): progress_bar.update() From d1800b73220bb49e8916ac58a8943ad3d768af9a Mon Sep 17 00:00:00 2001 From: Philippe CARL Date: Thu, 2 Jan 2025 19:33:36 +0100 Subject: [PATCH 03/42] Update __main__.py I erased my localize_info in lines 957 - 968 changes and left the rest --- picasso/__main__.py | 115 +++++++++++++++++++++++++++++++------------- 1 file changed, 82 insertions(+), 33 deletions(-) diff --git a/picasso/__main__.py b/picasso/__main__.py index ba901566..15e75443 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 @@ -954,34 +974,18 @@ def prompt_info(): except Exception as e: px = None + localize_info = { + "Generated by": "Picasso Localize", + "ROI": None, #TODO: change if ROI is given + "Box Size": box, + "Min. Net Gradient": min_net_gradient, + "Pixelsize": px, + "Fit method": args.fit_method + } + localize_info.update(camera_info) if args.fit_method == "mle": - localize_info = { - "Generated by": "Picasso Localize", - "ROI": None, - "Box Size": box, - "Min. Net Gradient": min_net_gradient, - "Pixelsize": px, - "Fit method": args.fit_method, - "Convergence Criterion": convergence, - "Max. Iterations": max_iterations, - "baseline": args.baseline, - "gain": args.gain, - "qe": args.qe, - "sensitivity": args.sensitivity, - } - else: - localize_info = { - "Generated by": "Picasso Localize", - "ROI": None, - "Box Size": box, - "Min. Net Gradient": min_net_gradient, - "Pixelsize": px, - "Fit method": args.fit_method, - "baseline": args.baseline, - "gain": args.gain, - "qe": args.qe, - "sensitivity": args.sensitivity, - } + localize_info["Convergence Criterion"] = convergence + localize_info["Max. Iterations"] = max_iterations if args.fit_method == "lq-3d" or args.fit_method == "lq-gpu-3d": print("------------------------------------------") @@ -1001,6 +1005,7 @@ def prompt_info(): print("------------------------------------------") info.append(localize_info) + info.append(camera_info) base, ext = splitext(path) @@ -1226,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", @@ -1255,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" @@ -1724,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": From 8f7e5ba5c62eb9918c0d14e626b1b040274fca9b Mon Sep 17 00:00:00 2001 From: Philippe CARL Date: Thu, 2 Jan 2025 19:35:18 +0100 Subject: [PATCH 04/42] Update __main__.py I erased my localize_info in lines 957 - 968 changes abd left the rest as requested From 565b4fbf13bc7040273a5692072f748cd2ef7050 Mon Sep 17 00:00:00 2001 From: Philippe CARL Date: Thu, 2 Jan 2025 19:38:37 +0100 Subject: [PATCH 05/42] Update gausslq.py I erase my changes in this file as requested --- picasso/gausslq.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/picasso/gausslq.py b/picasso/gausslq.py index d2165216..6a45d163 100644 --- a/picasso/gausslq.py +++ b/picasso/gausslq.py @@ -14,7 +14,7 @@ from tqdm import tqdm as _tqdm import numba as _numba import multiprocessing as _multiprocessing -from concurrent import futures as _futures +from concurrent import futures as _futures from . import postprocess as _postprocess try: @@ -81,6 +81,7 @@ def _initial_parameters(spot, size, size_half): def initial_parameters_gpufit(spots, size): + center = (size / 2.0) - 0.5 initial_width = _np.amax([size / 5.0, 1.0]) @@ -154,7 +155,7 @@ def fit_spot(spot): def fit_spots(spots): theta = _np.empty((len(spots), 6), dtype=_np.float32) - # theta.fill(_np.nan) + theta.fill(_np.nan) for i, spot in enumerate(spots): theta[i] = fit_spot(spot) return theta From 294f62daf51ab4698370b78e51f2b2ea1ebd8a21 Mon Sep 17 00:00:00 2001 From: Philippe CARL Date: Thu, 2 Jan 2025 19:44:45 +0100 Subject: [PATCH 06/42] Update localize.py I erase my changes in this file as requested --- picasso/localize.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/picasso/localize.py b/picasso/localize.py index 93f4dd7b..74e64f4c 100644 --- a/picasso/localize.py +++ b/picasso/localize.py @@ -344,6 +344,7 @@ def _cut_spots(movie, ids, box): return spots else: """Assumes that identifications are in order of frames!""" + r = int(box / 2) N = len(ids.frame) spots = _np.zeros((N, box, box), dtype=movie.dtype) @@ -354,11 +355,11 @@ def _cut_spots(movie, ids, box): def _to_photons(spots, camera_info): spots = _np.float32(spots) - baseline = camera_info["baseline"] - sensitivity = camera_info["sensitivity"] - gain = camera_info["gain"] + baseline = camera_info["Baseline"] + sensitivity = camera_info["Sensitivity"] + gain = camera_info["Gain"] # since v0.6.0: remove quantum efficiency to better reflect precision - # qe = camera_info["qe"] + # qe = camera_info["Qe"] return (spots - baseline) * sensitivity / (gain) @@ -561,4 +562,4 @@ def save_file_summary(summary): def add_file_to_db(file, file_hdf, drift=None, len_mean=None, nena=None): summary = get_file_summary(file, file_hdf, drift, len_mean, nena) - save_file_summary(summary) \ No newline at end of file + save_file_summary(summary) From 0b1ddff9ee9f2e918921e05bcaa289cc7fe55875 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Tue, 15 Oct 2024 18:26:27 +0200 Subject: [PATCH 07/42] 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 9626d883..c25917bb 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 e9df3587..8a4c2292 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -3935,7 +3935,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 @@ -4025,17 +4025,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 @@ -4047,17 +4047,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() @@ -4131,13 +4131,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 @@ -4193,13 +4194,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 333d8bbd788663eb31fb482dea5b3dc206481c44 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Tue, 15 Oct 2024 18:35:38 +0200 Subject: [PATCH 08/42] display the last loaded file in Render after closing a channel --- picasso/gui/render.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 8a4c2292..47d8652b 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -827,6 +827,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. """ @@ -5630,8 +5636,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 @@ -5733,10 +5738,14 @@ def add(self, path, render=True): self.window.dataset_dialog.add_entry(path) self.window.setWindowTitle( +<<<<<<< HEAD "Picasso v{}: Render. File: {}".format( # __version__, os.path.basename(path) __version__, path ) +======= + f"Picasso v{__version__}: Render. File: {os.path.basename(path)}" +>>>>>>> 6d49c52 (display the last loaded file in Render after closing a channel) ) # fast rendering add channel From 32bb0a55114fdfcf01b33afba9b58c86d53dfcb7 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Fri, 6 Dec 2024 09:07:01 +0100 Subject: [PATCH 09/42] 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 c25917bb..8b364034 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 47d8652b..12d3a3ba 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -9238,6 +9238,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 bd3f192c1d9dd94f0b16a68c21a4aa5ad0544946 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Fri, 6 Dec 2024 09:16:20 +0100 Subject: [PATCH 10/42] CMD localize saves camera information in the metadata file --- changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.rst b/changelog.rst index 8b364034..32497352 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 From 05b28c28fdbf75a88b01fe87d2a17d936d00e3f0 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Fri, 6 Dec 2024 09:27:21 +0100 Subject: [PATCH 11/42] 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 32497352..417c1585 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 12d3a3ba..08b49ca0 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -2858,8 +2858,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") @@ -2887,23 +2888,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") @@ -2924,23 +2928,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 5004931ee713c19ccf274efc405d1b1cf42503f9 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Fri, 6 Dec 2024 11:34:01 +0100 Subject: [PATCH 12/42] 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 417c1585..c432a1bf 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 08b49ca0..d8ab1609 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -3481,6 +3481,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() @@ -3491,12 +3494,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 """ @@ -3545,10 +3550,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, @@ -3808,93 +3810,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 3f2c4eef040194c018de5458d4e98fd91b77b1ca Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sun, 8 Dec 2024 23:31:00 +0100 Subject: [PATCH 13/42] 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 c432a1bf..c503d5fb 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 4336cb2c..b0f2217f 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 d8ab1609..0270ecc8 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -1711,7 +1711,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) @@ -10076,7 +10076,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, @@ -10100,7 +10100,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 9686201a0059dd325b6f88d9a9eefc1973a986a9 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sun, 8 Dec 2024 23:47:32 +0100 Subject: [PATCH 14/42] 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 0270ecc8..00c63415 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -2427,7 +2427,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) @@ -2436,7 +2435,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) @@ -2455,7 +2453,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) @@ -2463,7 +2460,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) @@ -2473,7 +2469,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) @@ -2493,7 +2488,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) @@ -2502,7 +2496,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) @@ -2511,7 +2504,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 fea65c1d79a625cefe04c1d591eb3a8d13fd8941 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Mon, 9 Dec 2024 00:00:21 +0100 Subject: [PATCH 15/42] 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 00c63415..f7b895cf 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -1831,12 +1831,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): @@ -1915,13 +1914,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): @@ -6062,9 +6060,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 @@ -6080,7 +6076,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( @@ -6090,9 +6086,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. @@ -6103,11 +6099,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 ) @@ -6136,7 +6135,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: @@ -6154,9 +6152,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 @@ -6172,7 +6168,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( @@ -6185,9 +6181,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. @@ -6198,11 +6202,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 9dfc0d042c14e37bc9105ecfa24978ebde1c8595 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Mon, 9 Dec 2024 00:34:06 +0100 Subject: [PATCH 16/42] AIM from CMD, remove --mode from CMD undrift --- changelog.rst | 1 + docs/cmd.rst | 4 ++++ picasso/aim.py | 4 +++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/changelog.rst b/changelog.rst index c503d5fb..0f99f1fe 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 4303efe5..ff5d7008 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/aim.py b/picasso/aim.py index b0f2217f..1010872d 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 b85fe94cd02dac5b484ab84630dbb242101b6863 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Mon, 9 Dec 2024 09:32:56 +0100 Subject: [PATCH 17/42] 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 f7b895cf..b2ce761e 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -5501,6 +5501,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) @@ -7013,6 +7015,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. """ @@ -7585,6 +7588,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: @@ -9692,6 +9696,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. """ @@ -10646,6 +10671,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 ea0ff5304d3e01c6735653655079acf3ea67b8b0 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sat, 14 Dec 2024 14:46:28 +0100 Subject: [PATCH 18/42] 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 0f99f1fe..b12cee39 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 b2ce761e..ed45c4b7 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -5388,6 +5388,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() @@ -7062,6 +7064,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. @@ -9150,7 +9205,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 @@ -10761,6 +10816,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() @@ -10904,6 +10961,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") @@ -11220,12 +11281,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(), @@ -11247,10 +11305,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]) @@ -11267,6 +11330,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 9ea8ca0a890dfe3404569482f2244c56685f86ed Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sat, 14 Dec 2024 16:05:48 +0100 Subject: [PATCH 19/42] 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 ed45c4b7..ef613687 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -7017,7 +7017,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. """ @@ -7643,7 +7642,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: @@ -10541,6 +10539,8 @@ def update_scene( picks_only=picks_only, ) self.update_cursor() + if not use_cache: + self.set_optimal_scalebar() def update_scene_slicer( self, @@ -10560,7 +10560,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 """ @@ -10726,7 +10726,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 8edfacbb8679e57f04cfa96cdf90134f4f4fc268 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sat, 14 Dec 2024 16:28:39 +0100 Subject: [PATCH 20/42] 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 4f61c818..61a61ac4 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 0c6b4d443349ba00e6bfe5f1fc7cebd46f7697b4 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sat, 14 Dec 2024 16:39:32 +0100 Subject: [PATCH 21/42] 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 61a61ac4..39dbcc09 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 1f7884ed4fe54e31257ee3bd9ef4b35cf016bdfa Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sat, 14 Dec 2024 16:40:52 +0100 Subject: [PATCH 22/42] CLEAN --- changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.rst b/changelog.rst index b12cee39..92401f3c 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 053b34770bcfe7fcc6532d25a1fb27b46b058327 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sat, 14 Dec 2024 16:43:53 +0100 Subject: [PATCH 23/42] =?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 4a32aa0c..44b55258 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 b348b3e8..97078804 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 3e5b94db..3c1d6945 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 13e48c82..eda0e15f 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 996c86f6..d1f00358 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 f935ae94..4be32332 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 23ba5673..6dd6d2d4 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 9e36657c..433cf9a7 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", From 88c6ccb8405988cf212683ce9fed517b62119989 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Sun, 15 Dec 2024 14:49:16 +0100 Subject: [PATCH 24/42] 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 7e39d7938b4baf98e55f4e7a2b16b19ae7b3613b Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Wed, 22 Jan 2025 11:09:48 +0100 Subject: [PATCH 25/42] 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 543a84905a13f8d5106982f76a5cb0035ca3f11c Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Wed, 22 Jan 2025 13:10:54 +0100 Subject: [PATCH 26/42] 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 0483bb256ecedf4657ee4e0b6472f6067e2cd4b6 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Wed, 5 Feb 2025 18:07:11 +0100 Subject: [PATCH 27/42] 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 018d546ed272848f67737a19b1e4737a6e21f638 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 10:16:45 +0100 Subject: [PATCH 28/42] 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 ef613687..c306292c 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -5820,7 +5820,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.""" @@ -5831,6 +5831,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 @@ -5842,8 +5845,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) @@ -9325,6 +9327,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 e491b1dfcd08a34fb13f63f5db23b7523e3eda2f Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 10:34:14 +0100 Subject: [PATCH 29/42] 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 d37dbd33e1f5c326ae8d3813d43f73d78f79c3ee Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 10:38:14 +0100 Subject: [PATCH 30/42] 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 c46a29d47ca09724facea55264ba8155fdc246df Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 10:40:05 +0100 Subject: [PATCH 31/42] 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 aa1ddfcd38cd62b59f5dd696a0984637f6cec9a0 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 10:42:56 +0100 Subject: [PATCH 32/42] 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 c306292c..23fea559 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -2898,6 +2898,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") @@ -2938,6 +2939,7 @@ def plot_2d(self, drift): ax2.set_xlabel("x (nm)") ax2.set_ylabel("y (nm)") + ax2.invert_yaxis() self.canvas.draw() From 597a3148dd0d4eb96f16920a8bd3f1d30f8cd4de Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 13:24:54 +0100 Subject: [PATCH 33/42] 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 078e7d947b8646809d0fbb783dbe952e19c2f7eb Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 14:24:58 +0100 Subject: [PATCH 34/42] 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 23fea559..15ee9060 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -5452,6 +5452,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() @@ -8717,6 +8719,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): """ @@ -11088,6 +11118,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 e1bdbaae3391574b5a2c7d2a0d78e76b221ef21b Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 15:36:57 +0100 Subject: [PATCH 35/42] 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 15ee9060..9c7b2d15 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -5556,8 +5556,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() @@ -9987,31 +9985,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 @@ -10029,85 +10015,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 4f21c905b2a35c6876c7d10c36ec6777f973f623 Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 15:38:00 +0100 Subject: [PATCH 36/42] =?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 44a800e72bdfe4880752b7b687f7d50c8c7ce50a Mon Sep 17 00:00:00 2001 From: rafalkowalewski1 Date: Thu, 6 Feb 2025 15:39:10 +0100 Subject: [PATCH 37/42] 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) From 515a40997873e85a3c26762500c111eb60bd3637 Mon Sep 17 00:00:00 2001 From: "Philippe Carl [IR CNRS] [Team D1MIGR]" Date: Fri, 14 Feb 2025 13:01:34 +0100 Subject: [PATCH 38/42] proposal for GUI improvements Within the main updates, both within the Localize and Render tools there are: - the cursor x, y positions as well as their values are displayed within the status bar - the mouse wheel zooms/unzooms centering on the mouse cursor position - it is as well possible to zoom by simply drawing a rectangle - as a left double-clicking is fully unzooming, i.e. similar to a View>Fit_image_to_window or Ctrl+w - and a right click and move able to drag the window in the case it is zoomed (with the open hand mouse cursor) Within the Render tool it is as well possible to set a pick with a Ctrl+Mouse_left_double_click even when being within the zoom mode. As within the Localize tool it is possible to draw a ROI by pressing the cursor key and drawing it. By making a mouse right click inside the ROI and moving it, the ROI will be displaced (with the closed hand mouse cursor) and outside the ROI it is the window that is displaced (with the open hand mouse cursor as described higher). Within the render tool, I added the possibility to enter/leave the 3D slicing mode with the Ctrl+Q shortkeys. Once in the 3D slicing mode, the given slice position (and size) is now indicated within the status_bar_frame_indicator and it is possible to change the slice position with a Ctrl+mouse_wheel. I also changed to "3D slicer menu" the original menu link within which I increased the size of the plot so that it is fully fitting within the window. What I had as well implemented previously and forgot to describe you is that it is possible to add a new pick with a Ctrl+left_mouse_double_click (within the Zoom mode) and erase a given pick with a Ctrl+right_mouse_double_click --- picasso/__main__.py | 38 +++++++++++++++++++++++++++----------- picasso/gausslq.py | 5 ++--- picasso/gui/render.py | 2 +- picasso/localize.py | 9 ++++----- picasso/zfit.py | 27 ++++++++++++++++++--------- 5 files changed, 52 insertions(+), 29 deletions(-) diff --git a/picasso/__main__.py b/picasso/__main__.py index 15e75443..d99c6767 100644 --- a/picasso/__main__.py +++ b/picasso/__main__.py @@ -974,18 +974,34 @@ def prompt_info(): except Exception as e: px = None - localize_info = { - "Generated by": "Picasso Localize", - "ROI": None, #TODO: change if ROI is given - "Box Size": box, - "Min. Net Gradient": min_net_gradient, - "Pixelsize": px, - "Fit method": args.fit_method - } - localize_info.update(camera_info) if args.fit_method == "mle": - localize_info["Convergence Criterion"] = convergence - localize_info["Max. Iterations"] = max_iterations + localize_info = { + "Generated by": "Picasso Localize", + "ROI": None, + "Box Size": box, + "Min. Net Gradient": min_net_gradient, + "Pixelsize": px, + "Fit method": args.fit_method, + "Convergence Criterion": convergence, + "Max. Iterations": max_iterations, + "baseline": args.baseline, + "gain": args.gain, + "qe": args.qe, + "sensitivity": args.sensitivity, + } + else: + localize_info = { + "Generated by": "Picasso Localize", + "ROI": None, + "Box Size": box, + "Min. Net Gradient": min_net_gradient, + "Pixelsize": px, + "Fit method": args.fit_method, + "baseline": args.baseline, + "gain": args.gain, + "qe": args.qe, + "sensitivity": args.sensitivity, + } if args.fit_method == "lq-3d" or args.fit_method == "lq-gpu-3d": print("------------------------------------------") diff --git a/picasso/gausslq.py b/picasso/gausslq.py index 6a45d163..d2165216 100644 --- a/picasso/gausslq.py +++ b/picasso/gausslq.py @@ -14,7 +14,7 @@ from tqdm import tqdm as _tqdm import numba as _numba import multiprocessing as _multiprocessing -from concurrent import futures as _futures +from concurrent import futures as _futures from . import postprocess as _postprocess try: @@ -81,7 +81,6 @@ def _initial_parameters(spot, size, size_half): def initial_parameters_gpufit(spots, size): - center = (size / 2.0) - 0.5 initial_width = _np.amax([size / 5.0, 1.0]) @@ -155,7 +154,7 @@ def fit_spot(spot): def fit_spots(spots): theta = _np.empty((len(spots), 6), dtype=_np.float32) - theta.fill(_np.nan) + # theta.fill(_np.nan) for i, spot in enumerate(spots): theta[i] = fit_spot(spot) return theta diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 9c7b2d15..55227f61 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -12485,4 +12485,4 @@ def excepthook(type, value, tback): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/picasso/localize.py b/picasso/localize.py index 74e64f4c..f2e3633a 100644 --- a/picasso/localize.py +++ b/picasso/localize.py @@ -344,7 +344,6 @@ def _cut_spots(movie, ids, box): return spots else: """Assumes that identifications are in order of frames!""" - r = int(box / 2) N = len(ids.frame) spots = _np.zeros((N, box, box), dtype=movie.dtype) @@ -355,11 +354,11 @@ def _cut_spots(movie, ids, box): def _to_photons(spots, camera_info): spots = _np.float32(spots) - baseline = camera_info["Baseline"] - sensitivity = camera_info["Sensitivity"] - gain = camera_info["Gain"] + baseline = camera_info["baseline"] + sensitivity = camera_info["sensitivity"] + gain = camera_info["gain"] # since v0.6.0: remove quantum efficiency to better reflect precision - # qe = camera_info["Qe"] + # qe = camera_info["qe"] return (spots - baseline) * sensitivity / (gain) diff --git a/picasso/zfit.py b/picasso/zfit.py index 099fafde..b79b58a6 100644 --- a/picasso/zfit.py +++ b/picasso/zfit.py @@ -23,11 +23,11 @@ def interpolate_nan(data): return data -def calibrate_z(locs, info, d, magnification_factor, path=None): +def calibrate_z(locs, info, min_net_gradient, step, magnification_factor, path=None): n_frames = info[0]["Frames"] - range = (n_frames - 1) * d + range = (n_frames - 1) * step frame_range = _np.arange(n_frames) - z_range = -(frame_range * d - range / 2) # negative so that the first frames of + z_range = -(frame_range * step - range / 2) # negative so that the first frames of # a bottom-to-up scan are positive z coordinates. mean_sx = _np.array([_np.mean(locs.sx[locs.frame == _]) for _ in frame_range]) @@ -52,21 +52,25 @@ def calibrate_z(locs, info, d, magnification_factor, path=None): cy = _np.polyfit(z_range, mean_sy, 6, full=False) # Fits calibration curve to each localization - # true_z = locs.frame * d - range / 2 + # true_z = locs.frame * step - range / 2 # cx = _np.polyfit(true_z, locs.sx, 6, full=False) # cy = _np.polyfit(true_z, locs.sy, 6, full=False) calibration = { - "X Coefficients": [float(_) for _ in cx], - "Y Coefficients": [float(_) for _ in cy], - "Number of frames": int(n_frames), - "Step size in nm": float(d), + "Calibration min. net gradient": int(min_net_gradient), "Magnification factor": float(magnification_factor), +<<<<<<< HEAD +======= + "Number of frames": int(n_frames), + "Step size in nm": float(step), + "X Coefficients": [float(_) for _ in cx], + "Y Coefficients": [float(_) for _ in cy] +>>>>>>> 2c0d57b (proposal for GUI improvements) } if path is not None: with open(path, "w") as f: - _yaml.dump(calibration, f, default_flow_style=False) + _yaml.dump(calibration, f, sort_keys=False, default_flow_style=False) locs = fit_z(locs, info, calibration, magnification_factor) locs.z /= magnification_factor @@ -252,6 +256,7 @@ def fit_z_parallel( int(n_locs / n_tasks + 1) if _ < n_locs % n_tasks else int(n_locs / n_tasks) for _ in range(n_tasks) ] + start_indices = _np.cumsum([0] + spots_per_task[:-1]) fs = [] executor = _ProcessPoolExecutor(n_workers) @@ -268,6 +273,10 @@ def fit_z_parallel( ) if asynch: return fs +<<<<<<< HEAD +======= +# with _tqdm( total=n_tasks, unit="task") as progress_bar: +>>>>>>> 2c0d57b (proposal for GUI improvements) with _tqdm(desc="3D Fitting", total=n_tasks, unit="task") as progress_bar: for f in _futures.as_completed(fs): progress_bar.update() From 371841087d43062b6d88440e210e1ba1538c8368 Mon Sep 17 00:00:00 2001 From: Philippe CARL Date: Thu, 2 Jan 2025 19:33:36 +0100 Subject: [PATCH 39/42] Update __main__.py I erased my localize_info in lines 957 - 968 changes and left the rest --- picasso/__main__.py | 38 +++++++++++--------------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/picasso/__main__.py b/picasso/__main__.py index d99c6767..15e75443 100644 --- a/picasso/__main__.py +++ b/picasso/__main__.py @@ -974,34 +974,18 @@ def prompt_info(): except Exception as e: px = None + localize_info = { + "Generated by": "Picasso Localize", + "ROI": None, #TODO: change if ROI is given + "Box Size": box, + "Min. Net Gradient": min_net_gradient, + "Pixelsize": px, + "Fit method": args.fit_method + } + localize_info.update(camera_info) if args.fit_method == "mle": - localize_info = { - "Generated by": "Picasso Localize", - "ROI": None, - "Box Size": box, - "Min. Net Gradient": min_net_gradient, - "Pixelsize": px, - "Fit method": args.fit_method, - "Convergence Criterion": convergence, - "Max. Iterations": max_iterations, - "baseline": args.baseline, - "gain": args.gain, - "qe": args.qe, - "sensitivity": args.sensitivity, - } - else: - localize_info = { - "Generated by": "Picasso Localize", - "ROI": None, - "Box Size": box, - "Min. Net Gradient": min_net_gradient, - "Pixelsize": px, - "Fit method": args.fit_method, - "baseline": args.baseline, - "gain": args.gain, - "qe": args.qe, - "sensitivity": args.sensitivity, - } + localize_info["Convergence Criterion"] = convergence + localize_info["Max. Iterations"] = max_iterations if args.fit_method == "lq-3d" or args.fit_method == "lq-gpu-3d": print("------------------------------------------") From c51fe476cea975d0157c983ed25850e08782d2e9 Mon Sep 17 00:00:00 2001 From: Philippe CARL Date: Thu, 2 Jan 2025 19:35:18 +0100 Subject: [PATCH 40/42] Update __main__.py I erased my localize_info in lines 957 - 968 changes abd left the rest as requested From f18b8d133635c55d05278a14f7a0f1ecd0d103de Mon Sep 17 00:00:00 2001 From: "Philippe Carl [IR CNRS] [Team D1MIGR]" Date: Fri, 14 Feb 2025 13:09:56 +0100 Subject: [PATCH 41/42] resolve conflict --- picasso/gui/render.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/picasso/gui/render.py b/picasso/gui/render.py index 55227f61..6ab97740 100644 --- a/picasso/gui/render.py +++ b/picasso/gui/render.py @@ -5755,14 +5755,7 @@ def add(self, path, render=True): self.window.dataset_dialog.add_entry(path) self.window.setWindowTitle( -<<<<<<< HEAD - "Picasso v{}: Render. File: {}".format( - # __version__, os.path.basename(path) - __version__, path - ) -======= f"Picasso v{__version__}: Render. File: {os.path.basename(path)}" ->>>>>>> 6d49c52 (display the last loaded file in Render after closing a channel) ) # fast rendering add channel From 973ce32864171e0cc37996ac99c410a3213042bb Mon Sep 17 00:00:00 2001 From: Philippe CARL Date: Fri, 14 Feb 2025 15:39:12 +0100 Subject: [PATCH 42/42] Update rotation.py --- picasso/gui/rotation.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/picasso/gui/rotation.py b/picasso/gui/rotation.py index f4e3a2c8..ae34aaa3 100644 --- a/picasso/gui/rotation.py +++ b/picasso/gui/rotation.py @@ -21,6 +21,29 @@ from numpy.lib.recfunctions import stack_arrays +from .. import io, render, lib""" + picasso/rotation + ~~~~~~~~~~~~~~~~~~~~ + + Rotation window classes and functions. + Extension of Picasso: Render to visualize 3D data. + Many functions are copied from gui.render.View to avoid circular import + + :author: Rafal Kowalewski, 2021-2022 + :copyright: Copyright (c) 2021 Jungmann Lab, MPI of Biochemistry +""" + +import os +import colorsys +from functools import partial + +import numpy as np +import matplotlib.pyplot as plt +import imageio.v2 as imageio +from PyQt5 import QtCore, QtGui, QtWidgets + +from numpy.lib.recfunctions import stack_arrays + from .. import io, render, lib @@ -2511,4 +2534,4 @@ def closeEvent(self, event): self.display_settings_dlg.close() self.animation_dialog.close() - QtWidgets.QMainWindow.closeEvent(self, event) \ No newline at end of file + QtWidgets.QMainWindow.closeEvent(self, event)