diff --git a/saenopy/examples.py b/saenopy/examples.py index fd11667..13da094 100644 --- a/saenopy/examples.py +++ b/saenopy/examples.py @@ -46,7 +46,10 @@ def download_files(url, target_folder=None, progress_callback=None): def load_example(name, target_folder=None, progress_callback=None, evaluated=False): if target_folder is None: target_folder = appdirs.user_data_dir("saenopy", "rgerum") - example = get_examples()[name] + try: + example = get_examples()[name] + except KeyError: + example = get_examples_2D()[name] url = example["url"] download_files(url, target_folder, progress_callback=progress_callback) @@ -124,3 +127,24 @@ def get_examples(): "url_evaluated_file": ["2023_02_14_12_0920_stack.saenopy"], }, } + + +def get_examples_2D(): + example_path = Path(appdirs.user_data_dir("saenopy", "rgerum")) + image_path = Path(resource_path("thumbnails")) + return { + "WTKO": { + "desc": "TODO", + "img": image_path / "liver_fibroblast_icon.png", + "pixel_size": 0.201, + "bf": example_path / 'WTKO/*/*_bf_before.tif', + "reference": example_path / 'WTKO/*/*_after.tif', + "deformed": example_path / 'WTKO/*/[0-9][0-9]_before.tif', + "output_path": example_path / 'WTKO/example_output', + "piv_parameters": {'window_size': 100, 'overlap': 60, 'std_factor': 15}, + "force_parameters": {'young': 49000, 'sigma': 0.49, 'h': 300}, + "url": "https://github.com/rgerum/saenopy/releases/download/v0.7.4/WTKO.zip", + "url_evaluated": "https://github.com/rgerum/saenopy/releases/download/v0.7.4/WTKO_evaluated.zip", + "url_evaluated_file": ["KO/04_bf_before.saenopy2D", "KO/05_bf_before.saenopy2D", "WT/03_bf_before.saenopy2D", "WT/10_bf_before.saenopy2D"], + }, + } diff --git a/saenopy/gui/common/QtShortCuts.py b/saenopy/gui/common/QtShortCuts.py index d196056..fc0dd82 100644 --- a/saenopy/gui/common/QtShortCuts.py +++ b/saenopy/gui/common/QtShortCuts.py @@ -744,6 +744,35 @@ def __exit__(self, exc_type, exc_val, exc_tb): pass +class QTabBarWidget(QtWidgets.QTabBar): + + def __init__(self, layout, *args, **kwargs): + super().__init__(*args, **kwargs) + if layout is None and current_layout is not None: + layout = current_layout + layout.addWidget(self) + self.widgets = [] + + + def createTab(self, name): + tab_stack = QtWidgets.QWidget() + self.widgets.append(tab_stack) + self.addTab(name) + v_layout = QVBoxLayout(tab_stack) + return v_layout + + def currentWidget(self): + return self.widgets[self.currentIndex()] + + def widget(self, i): + return self.widgets[i] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + class EnterableLayout: def __enter__(self): global current_layout diff --git a/saenopy/gui/tfm2d/__init__.py b/saenopy/gui/tfm2d/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/saenopy/gui/tfm2d/gui_2d.py b/saenopy/gui/tfm2d/gui_2d.py new file mode 100644 index 0000000..2d2ff6c --- /dev/null +++ b/saenopy/gui/tfm2d/gui_2d.py @@ -0,0 +1,42 @@ +import sys +from qtpy import QtCore, QtWidgets + +from saenopy.gui.common import QtShortCuts +from saenopy.gui.common.resources import resource_icon +from saenopy.gui.tfm2d.modules.BatchEvaluate import BatchEvaluate + +class MainWindowSolver(QtWidgets.QWidget): + + def __init__(self, parent=None): + super().__init__(parent) + + # QSettings + self.settings = QtCore.QSettings("Saenopy", "Saenopy") + + main_layout = QtWidgets.QHBoxLayout(self) + + with QtShortCuts.QTabWidget(main_layout) as self.tabs: + with self.tabs.createTab("Analyse Measurements"): + with QtShortCuts.QHBoxLayout(): + self.deformations = BatchEvaluate(self) + QtShortCuts.current_layout.addWidget(self.deformations) + + #with self.tabs.createTab("Data Analysis"): + # with QtShortCuts.QHBoxLayout(): + # self.plotting_window = PlottingWindow(self).addToLayout() + + +if __name__ == '__main__': # pragma: no cover + app = QtWidgets.QApplication(sys.argv) + if sys.platform.startswith('win'): + import ctypes + myappid = 'fabrylab.saenopy.master' # arbitrary string + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) + print(sys.argv) + window = MainWindowSolver() + window.setMinimumWidth(1600) + window.setMinimumHeight(900) + window.setWindowTitle("Saenopy Viewer") + window.setWindowIcon(resource_icon("Icon.ico")) + window.show() + sys.exit(app.exec_()) diff --git a/saenopy/gui/tfm2d/modules/BatchEvaluate.py b/saenopy/gui/tfm2d/modules/BatchEvaluate.py new file mode 100644 index 0000000..1de40bd --- /dev/null +++ b/saenopy/gui/tfm2d/modules/BatchEvaluate.py @@ -0,0 +1,433 @@ +import json +import sys +import os + +import qtawesome as qta +from qtpy import QtCore, QtWidgets, QtGui +import numpy as np +import glob +import threading +from pathlib import Path +import matplotlib as mpl + +import traceback + +from saenopy import get_stacks +from saenopy import Result +from saenopy.gui.tfm2d.modules.result import Result2D, get_stacks2D +from saenopy.gui.common import QtShortCuts +from saenopy.gui.common.gui_classes import ListWidget +from saenopy.gui.common.stack_selector_tif import add_last_voxel_size, add_last_time_delta + +#from .DeformationDetector import DeformationDetector +#from .FittedMesh import FittedMesh +#from .MeshCreator import MeshCreator +#from .Regularizer import Regularizer +#from .ResultView import ResultView +#from .StackDisplay import StackDisplay +#from saenopy.gui.solver.modules.exporter.Exporter import ExportViewer +from .load_measurement_dialog import AddFilesDialog, FileExistsDialog +from .draw import DrawWindow +from .DisplayBrightfield import DisplayBrightfield +from .DisplayRelaxed import DeformationDetector +from .DisplayDeformed import DeformationDetector2 +from .CalculateDisplacements import DeformationDetector3 +from .CalculateForces import Force +from .CalculateForceGeneration import ForceGeneration +from .CalculateStress import CalculateStress +#from .path_editor import start_path_change +from saenopy.examples import get_examples_2D + + +class SharedProperties: + properties = None + + def __init__(self): + self.properties = {} + + def add_property(self, name, target): + if name not in self.properties: + self.properties[name] = [] + self.properties[name].append(target) + + def change_property(self, name, value, target): + if name in self.properties: + for t in self.properties[name]: + if t != target: + t.property_changed(name, value) + + +class BatchEvaluate(QtWidgets.QWidget): + result_changed = QtCore.Signal(object) + tab_changed = QtCore.Signal(object) + set_current_result = QtCore.Signal(object) + + def __init__(self, parent=None): + super().__init__(parent) + + self.shared_properties = SharedProperties() + + self.settings = QtCore.QSettings("Saenopy", "Seanopy_deformation") + + with QtShortCuts.QHBoxLayout(self) as main_layout: + main_layout.setContentsMargins(0, 0, 0, 0) + with QtShortCuts.QSplitter() as lay: + with QtShortCuts.QVBoxLayout() as layout: + layout.setContentsMargins(0, 0, 0, 0) + self.list = ListWidget(layout, add_item_button="add measurements", copy_params=True, allow_paste_callback=self.allow_paste) + self.list.addItemClicked.connect(self.add_measurement) + self.list.signal_act_copy_clicked.connect(self.copy_params) + self.list.signal_act_paste_clicked.connect(self.paste_params) + self.list.signal_act_paths_clicked.connect(self.path_editor) + self.list.itemSelectionChanged.connect(self.listSelected) + self.progressbar = QtWidgets.QProgressBar().addToLayout() + self.progressbar.setOrientation(QtCore.Qt.Horizontal) + with QtShortCuts.QHBoxLayout() as layout: + layout.setContentsMargins(0, 0, 0, 0) + with QtShortCuts.QVBoxLayout() as layout: + layout.setContentsMargins(0, 0, 0, 0) + with QtShortCuts.QTabBarWidget(layout) as self.tabs: + self.tabs.setMinimumWidth(500) + old_tab = None + cam_pos = None + def tab_changed(x): + nonlocal old_tab, cam_pos + tab = self.tabs.currentWidget() + self.tab_changed.emit(tab) + self.tabs.currentChanged.connect(tab_changed) + pass + self.draw = DrawWindow(self, QtShortCuts.current_layout) + self.draw.signal_mask_drawn.connect(self.on_mask_drawn) + with QtShortCuts.QVBoxLayout() as layout0: + layout0.parent().setMaximumWidth(420) + layout0.setContentsMargins(0, 0, 0, 0) + self.sub_bf = DisplayBrightfield(self, layout0) + self.sub_draw = DeformationDetector(self, layout0) + self.sub_draw2 = DeformationDetector2(self, layout0) + self.sub_draw3 = DeformationDetector3(self, layout0) + self.sub_force = Force(self, layout0) + self.sub_force_gen = ForceGeneration(self, layout0) + self.sub_stress = CalculateStress(self, layout0) + #self.sub_module_stacks = StackDisplay(self, layout0) + #self.sub_module_deformation = DeformationDetector(self, layout0) + #self.sub_module_mesh = MeshCreator(self, layout0) + #self.sub_module_fitted_mesh = FittedMesh(self, layout0) + #self.sub_module_regularize = Regularizer(self, layout0) + #self.sub_module_view = ResultView(self, layout0) + #self.sub_module_fiber = FiberViewer(self, layout0) + #self.sub_module_export = ExportViewer(self, layout0) + layout0.addStretch() + + box = QtWidgets.QGroupBox("painting").addToLayout() + with QtShortCuts.QVBoxLayout(box) as layout: + self.slider_cursor_width = QtShortCuts.QInputNumber(None, "cursor width", 10, 1, 100, True, float=False) + self.slider_cursor_width.valueChanged.connect( lambda x: self.draw.setCursorSize(x)) + self.slider_cursor_opacity = QtShortCuts.QInputNumber(None, "mask opacity", 0.5, 0, 1, True, float=True) + self.slider_cursor_opacity.valueChanged.connect( lambda x: self.draw.setOpacity(x)) + with QtShortCuts.QHBoxLayout(): + self.button_red = QtShortCuts.QPushButton(None, "tractions", lambda x: self.draw.setColor(1), icon=qta.icon("fa5s.circle", color="red")) + self.button_green = QtShortCuts.QPushButton(None, "cell boundary", lambda x: self.draw.setColor(2), icon=qta.icon("fa5s.circle", color="green")) + #self.button_blue = QtShortCuts.QPushButton(None, "blue", lambda x: self.draw.setColor(3), icon=qta.icon("fa5s.circle", color="blue")) + self.button_start_all = QtShortCuts.QPushButton(None, "run all", self.run_all) + with QtShortCuts.QHBoxLayout(): + self.button_code = QtShortCuts.QPushButton(None, "export code", self.generate_code) + self.button_export = QtShortCuts.QPushButton(None, "export images", lambda x: self.sub_module_export.export_window.show()) + + self.data = [] + self.list.setData(self.data) + + self.setAcceptDrops(True) + + self.tasks = [] + self.current_task_id = 0 + self.thread = None + self.signal_task_finished.connect(self.run_finished) + + # load paths + self.load_from_path([arg for arg in sys.argv if arg.endswith(".saenopy2D")]) + + # disable all tabs + for i in range(self.tabs.count()-1, -1, -1): + self.tabs.setTabEnabled(i, False) + + def on_mask_drawn(self): + if self.result: + self.result.mask = self.draw.get_image() + + def copy_params(self): + result = self.list.data[self.list.currentRow()][2] + params = { + "piv_parameters": result.piv_parameters_tmp, + "force_parameters": result.force_parameters_tmp, + "force_gen_parameters": result.force_gen_parameters_tmp, + "stress_parameters": result.stress_parameters_tmp, + } + print(params) + for group in params: + if params[group] is None: + continue + for g in params[group]: + if type(params[group][g]) == np.bool_: + params[group][g] = bool(params[group][g]) + if type(params[group][g]) == np.int64: + params[group][g] = int(params[group][g]) + text = json.dumps(params, indent=2) + cb = QtGui.QGuiApplication.clipboard() + cb.setText(text, mode=cb.Clipboard) + + def allow_paste(self): + cb = QtGui.QGuiApplication.clipboard() + text = cb.text(mode=cb.Clipboard) + try: + data = json.loads(text) + if "piv_parameters" in data and \ + "mesh_parameters" in data and \ + "material_parameters" in data and \ + "solve_parameters" in data: + return True + except (ValueError, TypeError): + return False + return False + + def paste_params(self): + cb = QtGui.QGuiApplication.clipboard() + text = cb.text(mode=cb.Clipboard) + try: + data = json.loads(text) + except ValueError: + return False + result = self.list.data[self.list.currentRow()][2] + params = ["piv_parameters", "mesh_parameters", "material_parameters", "solve_parameters"] + for par in params: + if par in data: + setattr(result, par+"_tmp", data[par]) + self.set_current_result.emit(result) + + def path_editor(self): + result = self.list.data[self.list.currentRow()][2] + start_path_change(self, result) + + def progress(self, tup): + n, total = tup + self.progressbar.setMaximum(total) + self.progressbar.setValue(n) + + def generate_code(self): + new_path = QtWidgets.QFileDialog.getSaveFileName(None, "Save Session as Script", os.getcwd(), "Python File (*.py)") + if new_path: + # ensure filename ends in .py + if not new_path.endswith(".py"): + new_path += ".py" + + import_code = "" + run_code = "" + for module in [self.sub_module_stacks, self.sub_module_deformation, self.sub_module_mesh, self.sub_module_regularize]: + code1, code2 = module.get_code() + import_code += code1 + run_code += code2 +"\n" + run_code = import_code + "\n\n" + run_code + #print(run_code) + with open(new_path, "w") as fp: + fp.write(run_code) + + def run_all(self): + for i in range(len(self.data)): + if not self.data[i][1]: + continue + result = self.data[i][2] + if self.sub_module_deformation.group.value() is True: + self.sub_module_deformation.start_process(result=result) + if self.sub_module_mesh.group.value() is True: + self.sub_module_mesh.start_process(result=result) + if self.sub_module_regularize.group.value() is True: + self.sub_module_regularize.start_process(result=result) + + def addTask(self, task, result, params, name): + self.tasks.append([task, result, params, name]) + if self.thread is None: + self.run_next() + + signal_task_finished = QtCore.Signal() + + def run_next(self): + task, result, params, name = self.tasks[self.current_task_id] + self.thread = threading.Thread(target=self.run_thread, args=(task, result, params, name), daemon=True) + self.thread.start() + + def run_thread(self, task, result, params, name): + result.state = True + self.update_icons() + task(result, params) + self.signal_task_finished.emit() + result.state = False + self.update_icons() + + def run_finished(self): + self.current_task_id += 1 + self.thread = None + if self.current_task_id < len(self.tasks): + self.run_next() + + def dragEnterEvent(self, event: QtGui.QDragEnterEvent): + # accept url lists (files by drag and drop) + for url in event.mimeData().urls(): + # if str(url.toString()).strip().endswith(".npz"): + event.accept() + return + event.ignore() + + def dragMoveEvent(self, event: QtGui.QDragMoveEvent): + event.acceptProposedAction() + + def dropEvent(self, event: QtCore.QEvent): + urls = [] + for url in event.mimeData().urls(): + + url = url.toLocalFile() # path() + + if url[0] == "/" and url[2] == ":": + url = url[1:] + urls.append(url) + self.load_from_path(urls) + + def load_from_path(self, paths): + # make sure that paths is a list + if isinstance(paths, (str, Path)): + paths = [paths] + + # iterate over all paths + for path in paths: + # if it is a directory search all saenopy files in it + path = Path(path) + if path.is_dir(): + path = str(path) + "/**/*.saenopy2D" + # glob over the path (or just use the path if it does not contain a *) + for p in sorted(glob.glob(str(path), recursive=True)): + print(p) + try: + self.add_data(Result2D.load(p)) + except Exception as err: + QtWidgets.QMessageBox.critical(self, "Open Files", f"File {p} is not a valid Saenopy2D file.") + traceback.print_exc() + self.update_icons() + + def add_data(self, data): + self.list.addData(data.output, True, data, mpl.colors.to_hex(f"gray")) + + def update_icons(self): + for j in range(self.list.count( ) -1): + if self.data[j][2].state is True: + self.list.item(j).setIcon(qta.icon("fa5s.hourglass-half", options=[dict(color="orange")])) + else: + self.list.item(j).setIcon(qta.icon("fa5.circle", options=[dict(color="gray")])) + + def add_measurement(self): + last_decision = None + def do_overwrite(filename): + nonlocal last_decision + + # if we are in demo mode always load the files + if os.environ.get("DEMO") == "true": # pragma: no cover + return "read" + + # if there is a last decistion stored use that + if last_decision is not None: + return last_decision + + # ask the user if they want to overwrite or read the existing file + dialog = FileExistsDialog(self, filename) + result = dialog.exec() + # if the user clicked cancel + if not result: + return 0 + # if the user wants to remember the last decision + if dialog.use_for_all.value(): + last_decision = dialog.mode + # return the decision + return dialog.mode + + # getStack + dialog = AddFilesDialog(self, self.settings) + if not dialog.exec(): + return + + # create a new measurement object + if dialog.mode == "new": + # if there was a bf stack selected + bf_stack = dialog.stack_bf_input.value() + + # if there was a reference stack selected + reference_stack = dialog.stack_reference_input.value() + + # the active selected stack + active_stack = dialog.stack_data_input.value() + + try: + results = get_stacks2D(dialog.outputText.value(), + bf_stack, active_stack, reference_stack, pixel_size=dialog.pixel_size.value(), + exist_overwrite_callback=do_overwrite, + ) + #results = [ + # Result2D(dialog.outputText.value(), bf=bf_stack, + # input=active_stack, reference_stack=reference_stack, pixel_size=dialog.pixel_size.value())] + ## load the stack + #results = get_stacks( + # active_stack, + # reference_stack=reference_stack, + # output_path=dialog.outputText.value(), + # voxel_size=dialog.stack_data.getVoxelSize(), + # time_delta=time_delta, + # crop=dialog.stack_data.get_crop(), + # exist_overwrite_callback=do_overwrite, + #) + except Exception as err: + # notify the user if errors occured + QtWidgets.QMessageBox.critical(self, "Load Stacks", str(err)) + traceback.print_exc() + else: + # store the last voxel size + #add_last_voxel_size(dialog.stack_data.getVoxelSize()) + # add the loaded measruement objects + for data in results: + self.add_data(data) + + # load existing files + elif dialog.mode == "existing": + self.load_from_path(dialog.outputText3.value()) + + # load from the examples database + elif dialog.mode == "example": + # get the date from the example referenced by name + example = get_examples_2D()[dialog.mode_data] + + # generate a stack with the examples data + results = get_stacks2D( + example["output_path"], + example["bf"], + example["deformed"], + example["reference"], + example["pixel_size"], + exist_overwrite_callback=do_overwrite, + ) + # load all the measurement objects + for data in results: + if getattr(data, "is_read", False) is False: + data.piv_parameters = example["piv_parameters"] + data.force_parameters = example["force_parameters"] + self.add_data(data) + elif dialog.mode == "example_evaluated": + self.load_from_path(dialog.examples_output) + + # update the icons + self.update_icons() + + def listSelected(self): + if self.list.currentRow() is not None and self.list.currentRow() < len(self.data): + pipe = self.data[self.list.currentRow()][2] + if pipe.mask is None: + self.draw.setMask(np.zeros(pipe.shape, dtype=np.uint8)) + else: + self.draw.setMask(pipe.mask.astype(np.uint8)) + self.set_current_result.emit(pipe) + tab = self.tabs.currentWidget() + self.tab_changed.emit(tab) diff --git a/saenopy/gui/tfm2d/modules/CalculateDisplacements.py b/saenopy/gui/tfm2d/modules/CalculateDisplacements.py new file mode 100644 index 0000000..4662824 --- /dev/null +++ b/saenopy/gui/tfm2d/modules/CalculateDisplacements.py @@ -0,0 +1,89 @@ +import matplotlib.pyplot as plt +import numpy as np +from qtpy import QtCore, QtWidgets, QtGui +from saenopy.gui.common import QtShortCuts, QExtendedGraphicsView +from qimage2ndarray import array2qimage +import sys +import traceback +from PIL import Image, ImageDraw +from .PipelineModule import PipelineModule +from tifffile import imread +from saenopy.gui.common.gui_classes import CheckAbleGroup, QProcess, ProcessSimple +from .result import Result2D +from pyTFM.TFM_functions import calculate_deformation +from pyTFM.plotting import show_quiver +from pyTFM.frame_shift_correction import correct_stage_drift + + +class DeformationDetector3(PipelineModule): + + def __init__(self, parent=None, layout=None): + super().__init__(parent, layout) + self.parent = parent + #layout.addWidget(self) + with self.parent.tabs.createTab("Deformations") as self.tab: + pass + + with QtShortCuts.QVBoxLayout(self) as layout: + layout.setContentsMargins(0, 0, 0, 0) + with CheckAbleGroup(self, "find deformations (piv)").addToLayout() as self.group: + with QtShortCuts.QVBoxLayout() as layout: + with QtShortCuts.QHBoxLayout(): + self.input_win = QtShortCuts.QInputNumber(None, "window size", 100, float=False, + value_changed=self.valueChanged, unit="px", + tooltip="the size of the volume to look for a match") + self.input_overlap = QtShortCuts.QInputNumber(None, "overlap", 60, step=1, float=False, + value_changed=self.valueChanged, unit="px", + tooltip="the overlap of windows") + self.input_std = QtShortCuts.QInputNumber(None, "std_factor", 15, step=1, float=True, + value_changed=self.valueChanged, unit="px", + tooltip="additional filter for extreme values in deformation field") + self.label = QtWidgets.QLabel().addToLayout() + self.input_button = QtShortCuts.QPushButton(None, "detect deformations", self.start_process) + + self.setParameterMapping("piv_parameters", { + "window_size": self.input_win, + "overlap": self.input_overlap, + "std_factor": self.input_std + }) + + def valueChanged(self): + if self.check_available(self.result): + im = imread(self.result.reference_stack).shape + #voxel_size1 = self.result.stacks[0].voxel_size + #stack_deformed = self.result.stacks[0] + #overlap = 1 - (self.input_element_size.value() / self.input_win.value()) + #stack_size = np.array(stack_deformed.shape)[:3] * voxel_size1 - self.input_win.value() + #self.label.setText( + # f"""Overlap between neighbouring windows\n(size={self.input_win.value()}µm or {(self.input_win.value() / np.array(voxel_size1)).astype(int)} px) is choosen \n to {int(overlap * 100)}% for an element_size of {self.input_element_size.value():.1f}μm elements.\nTotal region is {stack_size}.""") + else: + self.label.setText("") + + def check_available(self, result): + return True + + def check_evaluated(self, result: Result2D) -> bool: + return result.u is not None + + def tabChanged(self, tab): + if self.tab is not None and self.tab.parent() == tab: + if self.check_evaluated(self.result): + im = self.result.get_deformation_field() + self.parent.draw.setImage(im*255) + + + def process(self, result: Result2D, piv_parameters: dict): # type: ignore + # result.reference_stack, result.input + u, v, mask_val, mask_std = calculate_deformation(result.get_image(1), result.get_image(0), window_size=piv_parameters["window_size"], overlap=piv_parameters["overlap"], std_factor=piv_parameters["std_factor"]) + result.u = -u + result.v = v + result.mask_val = mask_val + result.mask_std = mask_std + result.im_displacement = None + result.save() + print(u) + print(v) + print(mask_std) + print(mask_val) + fig1, ax = show_quiver(u, v, cbar_str="deformations\n[pixels]") + plt.savefig("deformation.png") diff --git a/saenopy/gui/tfm2d/modules/CalculateForceGeneration.py b/saenopy/gui/tfm2d/modules/CalculateForceGeneration.py new file mode 100644 index 0000000..ca67118 --- /dev/null +++ b/saenopy/gui/tfm2d/modules/CalculateForceGeneration.py @@ -0,0 +1,103 @@ +import matplotlib.pyplot as plt +from qtpy import QtCore, QtWidgets, QtGui +from saenopy.gui.common import QtShortCuts, QExtendedGraphicsView +from qimage2ndarray import array2qimage +import sys +import traceback +from PIL import Image, ImageDraw +from .PipelineModule import PipelineModule +from tifffile import imread +from saenopy.gui.common.gui_classes import CheckAbleGroup, QProcess, ProcessSimple +from .result import Result2D +from pyTFM.TFM_functions import TFM_tractions +from pyTFM.plotting import show_quiver +import numpy as np +from pyTFM.TFM_functions import strain_energy_points, contractillity +from scipy.ndimage.morphology import binary_fill_holes +from pyTFM.grid_setup_solids_py import interpolation # a simple function to resize the mask + + +class ForceGeneration(PipelineModule): + + def __init__(self, parent=None, layout=None): + super().__init__(parent, layout) + self.parent = parent + #layout.addWidget(self) + #with self.parent.tabs.createTab("Forces") as self.tab: + # pass + + with QtShortCuts.QVBoxLayout(self) as layout: + layout.setContentsMargins(0, 0, 0, 0) + with CheckAbleGroup(self, "force generation", url="https://saenopy.readthedocs.io/en/latest/interface_solver.html#detect-deformations").addToLayout() as self.group: + with QtShortCuts.QVBoxLayout(): + self.label = QtWidgets.QLabel("draw a mask with the red color to select the area where deformations and tractions that are generated by the colony.").addToLayout() + self.label.setWordWrap(True) + self.input_button = QtShortCuts.QPushButton(None, "calculate force generation", self.start_process) + + self.setParameterMapping("force_gen_parameters", {}) + + def valueChanged(self): + if self.check_available(self.result): + im = imread(self.result.reference_stack).shape + #voxel_size1 = self.result.stacks[0].voxel_size + #stack_deformed = self.result.stacks[0] + #overlap = 1 - (self.input_element_size.value() / self.input_win.value()) + #stack_size = np.array(stack_deformed.shape)[:3] * voxel_size1 - self.input_win.value() + #self.label.setText( + # f"""Overlap between neighbouring windows\n(size={self.input_win.value()}µm or {(self.input_win.value() / np.array(voxel_size1)).astype(int)} px) is choosen \n to {int(overlap * 100)}% for an element_size of {self.input_element_size.value():.1f}μm elements.\nTotal region is {stack_size}.""") + else: + self.label.setText("") + + def check_available(self, result): + return result.tx is not None + + def check_evaluated(self, result: Result2D) -> bool: + return result.tx is not None + + def tabChanged(self, tab): + if self.tab is not None and self.tab.parent() == tab: + if self.check_evaluated(self.result): + im = imread(self.result.reference_stack) + + fig1, ax = show_quiver(self.result.tx, self.result.ty, cbar_str="tractions\n[Pa]") + ax.set_position([0, 0, 1, 1]) + fig1.set_dpi(100) + fig1.set_size_inches(im.shape[1] / 100, im.shape[0] / 100) + plt.savefig("force.png") + im = plt.imread("force.png") + self.parent.draw.setImage(im*255) + + + def process(self, result: Result2D, force_gen_parameters: dict): # type: ignore + mask = binary_fill_holes(result.mask == 1) # the mask should be a single patch without holes + # changing the masks dimensions to fit to the deformation and traction fields + mask = interpolation(mask, dims=result.u.shape) + + ps1 = result.pixel_size # pixel size of the image of the beads + # dimensions of the image of the beads + ps2 = ps1 * np.mean(np.array(result.shape) / np.array(result.u.shape)) # pixel size of the deformation field + + # strain energy: + # first we calculate a map of strain energy + energy_points = strain_energy_points(result.u, result.v, result.tx, result.ty, ps1, ps2) # J/pixel + print(energy_points.shape) + print("v", result.v.shape) + print("u", result.u.shape) + print("tx", result.tx.shape) + print("ty", result.ty.shape) + plt.imsave("strain_energy.png", energy_points) + plt.imsave("mask.png", mask) + # then we sum all energy points in the area defined by mask + strain_energy = np.sum(energy_points[mask]) # 2.14*10**-13 J + + # contractility + contractile_force, proj_x, proj_y, center = contractillity(result.tx, result.ty, ps2, mask) # 2.03*10**-6 N + + print("strain energy", strain_energy) + print("contractile force", contractile_force) + print("projection", proj_x, proj_y) + print("projection", proj_x.shape, np.unique(proj_x), proj_y.shape, np.unique(proj_y)) + print("center", center) + + result.save() + diff --git a/saenopy/gui/tfm2d/modules/CalculateForces.py b/saenopy/gui/tfm2d/modules/CalculateForces.py new file mode 100644 index 0000000..7fa0bff --- /dev/null +++ b/saenopy/gui/tfm2d/modules/CalculateForces.py @@ -0,0 +1,90 @@ +import matplotlib.pyplot as plt +from qtpy import QtCore, QtWidgets, QtGui +from saenopy.gui.common import QtShortCuts, QExtendedGraphicsView +from qimage2ndarray import array2qimage +import sys +import traceback +from PIL import Image, ImageDraw +from .PipelineModule import PipelineModule +from tifffile import imread +from saenopy.gui.common.gui_classes import CheckAbleGroup, QProcess, ProcessSimple +from .result import Result2D +from pyTFM.TFM_functions import TFM_tractions +from pyTFM.plotting import show_quiver +import numpy as np + + +class Force(PipelineModule): + + def __init__(self, parent=None, layout=None): + super().__init__(parent, layout) + self.parent = parent + #layout.addWidget(self) + with self.parent.tabs.createTab("Forces") as self.tab: + pass + + with QtShortCuts.QVBoxLayout(self) as layout: + layout.setContentsMargins(0, 0, 0, 0) + with CheckAbleGroup(self, "calculate forces").addToLayout() as self.group: + with QtShortCuts.QVBoxLayout() as layout: + with QtShortCuts.QHBoxLayout(): + self.input_young = QtShortCuts.QInputNumber(None, "young", 49000, float=False, + value_changed=self.valueChanged, unit="Pa", + tooltip="the size of the volume to look for a match") + self.input_sigma = QtShortCuts.QInputNumber(None, "poisson ratio", 0.49, step=1, float=True, + value_changed=self.valueChanged, + tooltip="the overlap of windows") + self.input_h = QtShortCuts.QInputNumber(None, "h", 300, step=1, float=True, + value_changed=self.valueChanged, unit="µm", + tooltip="the overlap of windows") + self.label = QtWidgets.QLabel().addToLayout() + self.input_button = QtShortCuts.QPushButton(None, "calculate traction forces", self.start_process) + + self.setParameterMapping("force_parameters", { + "young": self.input_young, + "sigma": self.input_sigma, + "h": self.input_h, + }) + + def valueChanged(self): + if self.check_available(self.result): + im = imread(self.result.reference_stack).shape + #voxel_size1 = self.result.stacks[0].voxel_size + #stack_deformed = self.result.stacks[0] + #overlap = 1 - (self.input_element_size.value() / self.input_win.value()) + #stack_size = np.array(stack_deformed.shape)[:3] * voxel_size1 - self.input_win.value() + #self.label.setText( + # f"""Overlap between neighbouring windows\n(size={self.input_win.value()}µm or {(self.input_win.value() / np.array(voxel_size1)).astype(int)} px) is choosen \n to {int(overlap * 100)}% for an element_size of {self.input_element_size.value():.1f}μm elements.\nTotal region is {stack_size}.""") + else: + self.label.setText("") + + def check_available(self, result): + return result.u is not None + + def check_evaluated(self, result: Result2D) -> bool: + return result.tx is not None + + def tabChanged(self, tab): + if self.tab is not None and self.tab.parent() == tab: + if self.check_evaluated(self.result): + im = self.result.get_force_field() + self.parent.draw.setImage(im*255) + + + def process(self, result: Result2D, force_parameters: dict): # type: ignore + ps1 = result.pixel_size # pixel size of the image of the beads + # dimensions of the image of the beads + im1_shape = result.shape + ps2 = ps1 * np.mean(np.array(im1_shape) / np.array(result.u.shape)) # pixel size of of the deformation field + young = 49000 # Young's modulus of the substrate in Pa + sigma = 0.49 # Poisson's ratio of the substrate + h = 300 # height of the substrate in µm, "infinite" is also accepted + tx, ty = TFM_tractions(result.u, result.v, pixelsize1=ps1, pixelsize2=ps2, + h=force_parameters["h"], young=force_parameters["young"], sigma=force_parameters["sigma"]) + + result.tx = tx + result.ty = ty + result.im_force = None + result.save() + fig2, ax = show_quiver(tx, ty, cbar_str="tractions\n[Pa]") + plt.savefig("force.png") diff --git a/saenopy/gui/tfm2d/modules/CalculateStress.py b/saenopy/gui/tfm2d/modules/CalculateStress.py new file mode 100644 index 0000000..f7b8555 --- /dev/null +++ b/saenopy/gui/tfm2d/modules/CalculateStress.py @@ -0,0 +1,145 @@ +import matplotlib.pyplot as plt +from qtpy import QtCore, QtWidgets, QtGui +from saenopy.gui.common import QtShortCuts, QExtendedGraphicsView +from qimage2ndarray import array2qimage +import sys +import traceback +from PIL import Image, ImageDraw +from .PipelineModule import PipelineModule +from tifffile import imread +from saenopy.gui.common.gui_classes import CheckAbleGroup, QProcess, ProcessSimple +from .result import Result2D +from pyTFM.TFM_functions import TFM_tractions +from pyTFM.plotting import show_quiver +import numpy as np +from pyTFM.TFM_functions import strain_energy_points, contractillity +from scipy.ndimage.morphology import binary_fill_holes +from pyTFM.grid_setup_solids_py import interpolation # a simple function to resize the mask +from pyTFM.grid_setup_solids_py import prepare_forces +from pyTFM.grid_setup_solids_py import grid_setup, FEM_simulation +from pyTFM.grid_setup_solids_py import find_borders +from pyTFM.stress_functions import lineTension +from pyTFM.plotting import plot_continuous_boundary_stresses + + +class CalculateStress(PipelineModule): + + def __init__(self, parent=None, layout=None): + super().__init__(parent, layout) + self.parent = parent + #layout.addWidget(self) + with self.parent.tabs.createTab("Line Tension") as self.tab: + pass + + with QtShortCuts.QVBoxLayout(self) as layout: + layout.setContentsMargins(0, 0, 0, 0) + with CheckAbleGroup(self, "stress", url="https://saenopy.readthedocs.io/en/latest/interface_solver.html#detect-deformations").addToLayout() as self.group: + with QtShortCuts.QVBoxLayout(): + self.label = QtWidgets.QLabel( + "draw a mask with the red color to select an area slightly larger then the colony. Draw a mask with the green color to circle every single cell and mark their boundaries.").addToLayout() + self.label.setWordWrap(True) + self.input_button = QtShortCuts.QPushButton(None, "calculate stress & line tensions", self.start_process) + + self.setParameterMapping("stress_parameters", {}) + + def valueChanged(self): + if self.check_available(self.result): + im = imread(self.result.reference_stack).shape + #voxel_size1 = self.result.stacks[0].voxel_size + #stack_deformed = self.result.stacks[0] + #overlap = 1 - (self.input_element_size.value() / self.input_win.value()) + #stack_size = np.array(stack_deformed.shape)[:3] * voxel_size1 - self.input_win.value() + #self.label.setText( + # f"""Overlap between neighbouring windows\n(size={self.input_win.value()}µm or {(self.input_win.value() / np.array(voxel_size1)).astype(int)} px) is choosen \n to {int(overlap * 100)}% for an element_size of {self.input_element_size.value():.1f}μm elements.\nTotal region is {stack_size}.""") + else: + self.label.setText("") + + def check_available(self, result): + return result.tx is not None + + def check_evaluated(self, result: Result2D) -> bool: + return result.im_tension is not None + + def tabChanged(self, tab): + if self.tab is not None and self.tab.parent() == tab: + if self.check_evaluated(self.result): + im = self.result.im_tension + self.parent.draw.setImage(im*255) + + + def process(self, result: Result2D, stress_parameters: dict): # type: ignore + ps1 = result.pixel_size # pixel size of the image of the beads + # dimensions of the image of the beads + ps2 = ps1 * np.mean(np.array(result.shape) / np.array(result.u.shape)) # pixel size of the deformation field + + # first mask: The area used for Finite Elements Methods + # it should encircle all forces generated by the cell colony + mask_FEM = binary_fill_holes(result.mask == 1) # the mask should be a single patch without holes + # changing the masks dimensions to fit to the deformation and traction field: + mask_FEM = interpolation(mask_FEM, dims=result.tx.shape) + + # second mask: The area of the cells. Average stresses and other values are calculated only + # on the actual area of the cell, represented by this mask. + mask_cells = binary_fill_holes(result.mask == 2) + mask_cells = interpolation(mask_cells, dims=result.tx.shape) + + # converting tractions (forces per surface area) to actual forces + # and correcting imbalanced forces and torques + # tx->traction forces in x direction, ty->traction forces in y direction + # ps2->pixel size of the traction field, mask_FEM-> mask for FEM + fx, fy = prepare_forces(result.tx, result.ty, ps2, mask_FEM) + result.fx = fx + result.fy = fy + + # constructing the FEM grid + nodes, elements, loads, mats = grid_setup(mask_FEM, -fx, -fy, sigma=0.5) + # performing the FEM analysis + # verbose prints the progress of numerically solving the FEM system of equations. + UG_sol, stress_tensor = FEM_simulation(nodes, elements, loads, mats, mask_FEM, verbose=True) + # UG_sol is a list of deformations for each node. We don't need it here. + + # mean normal stress + ms_map = ((stress_tensor[:, :, 0, 0] + stress_tensor[:, :, 1, 1]) / 2) / (ps2 * 10 ** -6) + # average on the area of the cell colony. + ms = np.mean(ms_map[mask_cells]) # 0.0043 N/m + + # coefficient of variation + cv = np.nanstd(ms_map[mask_cells]) / np.abs(np.nanmean(ms_map[mask_cells])) # 0.41 no unit + + result.ms = ms + result.cv = cv + + """ Calculating the Line Tension """ + # identifying borders, counting cells, performing spline interpolation to smooth the borders + borders = find_borders(result.mask == 2, result.tx.shape) + # we can for example get the number of cells from the "borders" object + n_cells = borders.n_cells # 8 + + # calculating the line tension along the cell borders + lt, min_v, max_v = lineTension(borders.lines_splines, borders.line_lengths, stress_tensor, pixel_length=ps2) + # lt is a nested dictionary. The first key is the id of a cell border. + # For each cell border the line tension vectors ("t_vecs"), the normal + # and shear component of the line tension ("t_shear") and the normal + # vectors of the cell border ("n_vecs") are calculated at a large number of points. + + # average norm of the line tension. Only borders not at colony edge are used + lt_vecs = np.concatenate([lt[l_id]["t_vecs"] for l_id in lt.keys() if l_id not in borders.edge_lines]) + avg_line_tension = np.mean(np.linalg.norm(lt_vecs, axis=1)) # 0.00569 N/m + + # average normal component of the line tension + lt_normal = np.concatenate([lt[l_id]["t_normal"] for l_id in lt.keys() if l_id not in borders.edge_lines]) + avg_normal_line_tension = np.mean(np.abs(lt_normal)) # 0.00566 N/m, + # here you can see that almost the line tensions act almost exclusively perpendicular to the cell borders. + + # plotting the line tension + fig3, ax = plot_continuous_boundary_stresses([borders.inter_shape, borders.edge_lines, lt, min_v, max_v], + cbar_style="outside") + plt.savefig("line_tension.png") + ax.set_position([0, 0, 1, 1]) + fig3.set_dpi(100) + fig3.set_size_inches(result.shape[1] / 100, result.shape[0] / 100) + plt.savefig("tension.png") + im = plt.imread("tension.png") + result.im_tension = im + + result.save() \ No newline at end of file diff --git a/saenopy/gui/tfm2d/modules/DisplayBrightfield.py b/saenopy/gui/tfm2d/modules/DisplayBrightfield.py new file mode 100644 index 0000000..8565232 --- /dev/null +++ b/saenopy/gui/tfm2d/modules/DisplayBrightfield.py @@ -0,0 +1,19 @@ +from .PipelineModule import PipelineModule +from tifffile import imread + + +class DisplayBrightfield(PipelineModule): + + def __init__(self, parent=None, layout=None): + super().__init__(parent, layout) + self.parent = parent + with self.parent.tabs.createTab("Brightfield") as self.tab: + pass + + def check_evaluated(self, result): + return True + + def tabChanged(self, tab): + if self.tab is not None and self.tab.parent() == tab: + im = self.result.get_image(-1) + self.parent.draw.setImage(im, self.result.shape) diff --git a/saenopy/gui/tfm2d/modules/DisplayDeformed.py b/saenopy/gui/tfm2d/modules/DisplayDeformed.py new file mode 100644 index 0000000..8ee5f50 --- /dev/null +++ b/saenopy/gui/tfm2d/modules/DisplayDeformed.py @@ -0,0 +1,42 @@ +from .PipelineModule import PipelineModule +from tifffile import imread +from qtpy import QtCore, QtWidgets, QtGui +from saenopy.gui.common.gui_classes import CheckAbleGroup, QProcess, ProcessSimple +from saenopy.gui.common import QtShortCuts, QExtendedGraphicsView +from pyTFM.frame_shift_correction import correct_stage_drift +from .result import Result2D +from pathlib import Path + + +class DeformationDetector2(PipelineModule): + + def __init__(self, parent=None, layout=None): + super().__init__(parent, layout) + self.parent = parent + with self.parent.tabs.createTab("Deformed") as self.tab: + pass + + with QtShortCuts.QVBoxLayout(self) as layout: + layout.setContentsMargins(0, 0, 0, 0) + with CheckAbleGroup(self, "drift").addToLayout() as self.group: + with QtShortCuts.QVBoxLayout(): + self.input_button = QtShortCuts.QPushButton(None, "calculate drift correction", self.start_process) + + self.setParameterMapping("drift_parameters", {}) + + def check_evaluated(self, result): + return True + + def tabChanged(self, tab): + if self.tab is not None and self.tab.parent() == tab: + im = self.result.get_image(1) + self.parent.draw.setImage(im, self.result.shape) + + def check_available(self, result): + return True + + def process(self, result: Result2D, drift_parameters: dict): # type: ignore + b_save, a_save, [], drift = correct_stage_drift(result.get_image(1, corrected=False), result.get_image(0, corrected=False)) + + b_save.save(result.input_corrected) + a_save.save(result.reference_stack_corrected) \ No newline at end of file diff --git a/saenopy/gui/tfm2d/modules/DisplayRelaxed.py b/saenopy/gui/tfm2d/modules/DisplayRelaxed.py new file mode 100644 index 0000000..50dc4b1 --- /dev/null +++ b/saenopy/gui/tfm2d/modules/DisplayRelaxed.py @@ -0,0 +1,18 @@ +from .PipelineModule import PipelineModule +from tifffile import imread + +class DeformationDetector(PipelineModule): + + def __init__(self, parent=None, layout=None): + super().__init__(parent, layout) + self.parent = parent + with self.parent.tabs.createTab("Referenece") as self.tab: + pass + + def check_evaluated(self, result): + return True + + def tabChanged(self, tab): + if self.tab is not None and self.tab.parent() == tab: + im = self.result.get_image(0) + self.parent.draw.setImage(im, self.result.shape) diff --git a/saenopy/gui/tfm2d/modules/PipelineModule.py b/saenopy/gui/tfm2d/modules/PipelineModule.py new file mode 100644 index 0000000..397ba14 --- /dev/null +++ b/saenopy/gui/tfm2d/modules/PipelineModule.py @@ -0,0 +1,254 @@ +import qtawesome as qta +from qtpy import QtCore, QtWidgets +from .result import Result2D +from typing import Tuple, List +import traceback + + +class ParameterMapping: + params_name: str = None + parameter_dict: dict = None + + result: Result2D = None + + def __init__(self, params_name: str = None, parameter_dict: dict=None): + self.params_name = params_name + self.parameter_dict = parameter_dict + for name, widget in self.parameter_dict.items(): + widget.valueChanged.connect(lambda x, name=name: self.setParameter(name, x)) + + self.setResult(None) + + def setParameter(self, name: str, value): + if self.result is not None: + getattr(self.result, self.params_name + "_tmp")[name] = value + + def ensure_tmp_params_initialized(self, result): + if self.params_name is None: + return + # if the results instance does not have the parameter dictionary yet, create it + if getattr(result, self.params_name + "_tmp", None) is None: + setattr(result, self.params_name + "_tmp", {}) + + # set the widgets to the value if the value exits + params = getattr(result, self.params_name) + params_tmp = getattr(result, self.params_name + "_tmp") + # iterate over the parameters + for name, widget in self.parameter_dict.items(): + if name not in params_tmp: + if params is not None and name in params: + params_tmp[name] = params[name] + else: + params_tmp[name] = widget.value() + + def setDisabled(self, disabled): + # disable all the widgets + for name, widget in self.parameter_dict.items(): + widget.setDisabled(disabled) + + def setResult(self, result: Result2D): + """ set a new active result object """ + self.result = result + + # if a result file is given + if result is not None: + self.ensure_tmp_params_initialized(result) + params_tmp = getattr(result, self.params_name + "_tmp") + # iterate over the parameters + for name, widget in self.parameter_dict.items(): + # set the widgets to the value if the value exits + widget.setValue(params_tmp[name]) + + +class PipelineModule(QtWidgets.QWidget): + processing_finished = QtCore.Signal() + processing_progress = QtCore.Signal(tuple) + processing_state_changed = QtCore.Signal(object) + processing_error = QtCore.Signal(str) + result: Result2D = None + tab: QtWidgets.QTabWidget = None + + parameter_mappings: List[ParameterMapping] = None + params_name: None + + def __init__(self, parent: "BatchEvaluate", layout): + super().__init__() + self.parameter_mappings = [] + self.params_name = None + + if layout is not None: + layout.addWidget(self) + if parent is None: + return + self.parent = parent + self.settings = self.parent.settings + + self.processing_finished.connect(self.finished_process) + self.processing_error.connect(self.errored_process) + self.processing_state_changed.connect(self.state_changed) + + self.parent.result_changed.connect(self.resultChanged) + self.parent.set_current_result.connect(self.setResult) + self.parent.tab_changed.connect(self.tabChanged) + + self.processing_progress.connect(self.parent.progress) + + def setParameterMapping(self, params_name: str = None, parameter_dict: dict=None): + self.params_name = params_name + if params_name is None: + return + self.parameter_mappings.append(ParameterMapping(params_name, parameter_dict)) + + current_result_plotted = False + current_tab_selected = False + def tabChanged(self, tab): + if self.tab is not None and self.tab.parent() == tab: + self.current_tab_selected = True + if self.current_result_plotted is False: + self.update_display() + self.current_result_plotted = True + else: + self.current_tab_selected = False + + def check_available(self, result: Result2D) -> bool: + return False + + def check_evaluated(self, result: Result2D) -> bool: + return False + + def resultChanged(self, result: Result2D): + """ called when the contents of result changed. Only update view if it is the currently displayed one. """ + if result is self.result: + if self.tab is not None: + for i in range(self.parent.tabs.count()): + if self.parent.tabs.widget(i) == self.tab.parent(): + self.parent.tabs.setTabEnabled(i, self.check_evaluated(result)) + if self.current_tab_selected is True: + self.update_display() + self.state_changed(result) + + def state_changed(self, result: Result2D): + if result is self.result and getattr(self, "group", None) is not None: + state = getattr(result, self.params_name + "_state", "") + if state == "scheduled": + self.group.label.setIcon(qta.icon("fa5s.hourglass-start", options=[dict(color="gray")])) + self.group.label.setToolTip("scheduled") + elif state == "running": + self.group.label.setIcon(qta.icon("fa5s.hourglass-half", options=[dict(color="orange")])) + self.group.label.setToolTip("running") + elif state == "finished": + self.group.label.setIcon(qta.icon("fa5s.hourglass-end", options=[dict(color="green")])) + self.group.label.setToolTip("finished") + elif state == "failed": + self.group.label.setIcon(qta.icon("fa5s.times", options=[dict(color="red")])) + self.group.label.setToolTip("failed") + else: + self.group.label.setIcon(qta.icon("fa5.circle", options=[dict(color="gray")])) + self.group.label.setToolTip("") + + if state == "scheduled" or state == "running": + # if not disable all the widgets + for mapping in self.parameter_mappings: + mapping.setDisabled(True) + if getattr(self, "input_button", None): + self.input_button.setEnabled(False) + else: + # if not disable all the widgets + for mapping in self.parameter_mappings: + mapping.setDisabled(False) + if getattr(self, "input_button", None): + self.input_button.setEnabled(self.check_available(result)) + #if getattr(self, "input_button", None): + # self.input_button.setEnabled(self.check_available(result)) + + def setResult(self, result: Result2D): + """ set a new active result object """ + #if result == self.result: + # return + self.current_result_plotted = False + self.result = result + + for mapping in self.parameter_mappings: + mapping.setResult(result) + + self.state_changed(result) + if self.tab is not None: + for i in range(self.parent.tabs.count()): + if self.parent.tabs.widget(i) == self.tab.parent(): + self.parent.tabs.setTabEnabled(i, self.check_evaluated(result)) + + # check if the results instance can be evaluated currently with this module + #if self.check_available(result) is False: + if getattr(self, "input_button", None): + self.input_button.setEnabled(self.check_available(result)) + if result is None or \ + (self.params_name and (getattr(result, self.params_name + "_state", "") == "scheduled" + or getattr(result, self.params_name + "_state", "") == "running")): + # if not disable all the widgets + for mapping in self.parameter_mappings: + mapping.setDisabled(True) + if getattr(self, "input_button", None): + self.input_button.setEnabled(False) + else: + # if not disable all the widgets + for mapping in self.parameter_mappings: + mapping.setDisabled(False) + self.valueChanged() + if self.current_tab_selected is True: + self.update_display() + + def update_display(self): + pass + + def valueChanged(self): + pass + + def start_process(self, x=None, result=None): + if result is None: + result = self.result + if result is None: + return + if getattr(result, self.params_name + "_state", "") == "scheduled" or \ + getattr(result, self.params_name + "_state", "") == "running": + return + + params = {} + for mapping in self.parameter_mappings: + mapping.ensure_tmp_params_initialized(result) + params[mapping.params_name] = getattr(result, mapping.params_name + "_tmp") + setattr(result, self.params_name + "_state", "scheduled") + self.processing_state_changed.emit(result) + return self.parent.addTask(self.process_thread, result, params, "xx") + + def process_thread(self, result: Result2D, params: dict): + #params = getattr(result, self.params_name + "_tmp") + self.parent.progressbar.setRange(0, 0) + setattr(result, self.params_name + "_state", "running") + self.processing_state_changed.emit(result) + try: + self.process(result, **params) + # store the parameters that have been used for evaluation + for mapping in self.parameter_mappings: + setattr(result, mapping.params_name, params[mapping.params_name].copy()) + result.save() + setattr(result, self.params_name + "_state", "finished") + self.parent.result_changed.emit(result) + self.processing_finished.emit() + except Exception as err: + traceback.print_exc() + setattr(result, self.params_name + "_state", "failed") + self.processing_state_changed.emit(result) + self.processing_error.emit(str(err)) + + def process(self, result: Result2D, params: dict): + pass + + def finished_process(self): + self.parent.progressbar.setRange(0, 1) + + def errored_process(self, text: str): + QtWidgets.QMessageBox.critical(self, "Deformation Detector", text) + self.parent.progressbar.setRange(0, 1) + + def get_code(self) -> Tuple[str, str]: + return "", "" diff --git a/saenopy/gui/tfm2d/modules/__init__.py b/saenopy/gui/tfm2d/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/saenopy/gui/tfm2d/modules/draw.py b/saenopy/gui/tfm2d/modules/draw.py new file mode 100644 index 0000000..02b8ada --- /dev/null +++ b/saenopy/gui/tfm2d/modules/draw.py @@ -0,0 +1,231 @@ +import os + +import matplotlib.pyplot as plt +from qtpy import QtCore, QtWidgets, QtGui +from saenopy.gui.common import QtShortCuts, QExtendedGraphicsView +from qimage2ndarray import array2qimage +import sys +import traceback +from PIL import Image, ImageDraw +import numpy as np + + +class GraphicsItemEventFilter(QtWidgets.QGraphicsItem): + def __init__(self, parent, command_object): + super(GraphicsItemEventFilter, self).__init__(parent) + self.commandObject = command_object + self.active = True + + def paint(self, *args): + pass + + def boundingRect(self): + return QtCore.QRectF(0, 0, 0, 0) + + def sceneEventFilter(self, scene_object, event): + if not self.active: + return False + return self.commandObject.sceneEventFilter(event) + + +class DrawWindow(QtWidgets.QWidget): + signal_mask_drawn = QtCore.Signal() + + def __init__(self, parent=None, layout=None): + super().__init__(parent) + if layout is not None: + layout.addWidget(self) + + im = np.zeros((100, 100, 3), dtype=np.uint8) + #im = plt.imread("/home/richard/PycharmProjects/pyTFM/example_data_for_pyTFM-master/python_tutorial/04before.tif") + with QtShortCuts.QVBoxLayout(self) as main_layout: + main_layout.setContentsMargins(0, 0, 0, 0) + self.view1 = QExtendedGraphicsView.QExtendedGraphicsView().addToLayout() + self.view1.setMinimumWidth(300) + self.pixmap_image = QtWidgets.QGraphicsPixmapItem(self.view1.origin) + self.pixmap_mask = QtWidgets.QGraphicsPixmapItem(self.view1.origin) + + self.pixmap_image.setPixmap(QtGui.QPixmap(array2qimage(im * 255))) + self.view1.setExtend(im.shape[1], im.shape[0]) + + self.scene_event_filter = GraphicsItemEventFilter(self.pixmap_image, self) + self.scene_event_filter.commandObject = self + self.pixmap_image.setAcceptHoverEvents(True) + self.pixmap_image.installSceneEventFilter(self.scene_event_filter) + + self.DrawCursor = QtWidgets.QGraphicsPathItem(self.view1.origin) + self.DrawCursor.setZValue(10) + #self.DrawCursor.setVisible(False) + + self.full_image = Image.new("I", im.shape[:2][::-1]) + + self.cursor_size = 10 + self.color = 1 + self.mask_opacity = 0.5 + + self.palette = np.zeros((256, 4), dtype=np.uint8) + self.palette[0, :] = [0, 0, 0, 0] + self.palette[1, :] = [255, 0, 0, 255] + self.palette[2, :] = [0, 255, 0, 255] + self.palette[3, :] = [0, 0, 255, 255] + + self.UpdateDrawCursorDisplay() + + def setColor(self, color): + self.color = color + self.UpdateDrawCursorDisplay() + + def setMask(self, mask): + self.full_image = Image.fromarray(mask.astype(np.uint8)) + im = np.asarray(self.full_image) + im = self.palette[im] + self.pixmap_mask.setPixmap(QtGui.QPixmap(array2qimage(im))) + self.view1.setExtend(mask.shape[1], mask.shape[0]) + self.changeOpacity(0) + + def setImage(self, im, shape=None): + if shape is not None and (shape[0] != self.full_image.height or shape[1] != self.full_image.width): + self.full_image = Image.new("I", shape[:2][::-1]) + self.pixmap_image.setPixmap(QtGui.QPixmap(array2qimage(im))) + self.view1.setExtend(im.shape[1], im.shape[0]) + + def setOpacity(self, value): + self.mask_opacity = np.clip(value, 0, 1) + self.pixmap_mask.setOpacity(self.mask_opacity) + + def changeOpacity(self, value: float) -> None: + # alter the opacity by value + self.mask_opacity += value + # the opacity has to be maximally 1 + if self.mask_opacity >= 1: + self.mask_opacity = 1 + # and minimally 0 + if self.mask_opacity < 0: + self.mask_opacity = 0 + # set the opacity + self.pixmap_mask.setOpacity(self.mask_opacity) + + def DrawLine(self, x1: float, x2: float, y1: float, y2: float, line_type: int = 1) -> None: + size = self.cursor_size + if line_type == 0: + color = 0 + else: + color = self.color#.index + draw = ImageDraw.Draw(self.full_image) + draw.line((x1, y1, x2, y2), fill=color, width=size + 1) + draw.ellipse((x1 - size // 2, y1 - size // 2, x1 + size // 2, y1 + size // 2), fill=color) + + import numpy as np + im = np.asarray(self.full_image) + im = self.palette[im] + self.pixmap_mask.setPixmap(QtGui.QPixmap(array2qimage(im))) + self.changeOpacity(0) + self.signal_mask_drawn.emit() + + def get_image(self): + return np.asarray(self.full_image) + + def setCursorSize(self, size): + self.cursor_size = size + self.UpdateDrawCursorDisplay() + + def changeCursorSize(self, size): + self.cursor_size += size + self.UpdateDrawCursorDisplay() + + def UpdateDrawCursorDisplay(self) -> None: + # update color and size of brush cursor + draw_cursor_path = QtGui.QPainterPath() + draw_cursor_path.addEllipse(-self.cursor_size * 0.5, -self.cursor_size * 0.5, self.cursor_size, + self.cursor_size) + pen = QtGui.QPen(QtGui.QColor(*self.palette[self.color, 0:3])) + pen.setCosmetic(True) + self.DrawCursor.setPen(pen) + self.DrawCursor.setPath(draw_cursor_path) + + def sceneEventFilter(self, event: QtCore.QEvent) -> bool: + if event.type() == QtCore.QEvent.GraphicsSceneMousePress and event.button() == QtCore.Qt.LeftButton: + # store the coordinates + self.last_pos = [event.pos().x(), event.pos().y()] + paint = event.modifiers() != QtCore.Qt.AltModifier + # add a first circle (so that even if the mouse isn't moved something is drawn) + self.DrawLine(self.last_pos[0], self.last_pos[0] + 0.00001, self.last_pos[1], self.last_pos[1], paint) + # accept the event + return True + if event.type() == QtCore.QEvent.GraphicsSceneMouseRelease and event.button() == QtCore.Qt.LeftButton: + pass + # Mouse move event to draw the stroke + if event.type() == QtCore.QEvent.GraphicsSceneMouseMove: + pos = [event.pos().x(), event.pos().y()] + # draw a line and store the position + paint = event.modifiers() != QtCore.Qt.AltModifier + self.DrawLine(pos[0], self.last_pos[0], pos[1], self.last_pos[1], paint) + self.last_pos = pos + self.DrawCursor.setPos(event.pos()) + # accept the event + return True + # Mouse hover updates the color_under_cursor and displays the brush cursor + if event.type() == QtCore.QEvent.GraphicsSceneHoverMove: + # move brush cursor + self.DrawCursor.setPos(event.pos()) + if event.type() == QtCore.QEvent.GraphicsSceneWheel: + try: # PyQt 5 + angle = event.angleDelta().y() + except AttributeError: # PyQt 4 + angle = event.delta() + # wheel with CTRL means changing the cursor size + if event.modifiers() == QtCore.Qt.ControlModifier: + if angle > 0: + self.changeCursorSize(+1) + else: + self.changeCursorSize(-1) + event.accept() + return True + # wheel with SHIFT means changing the opacity + elif event.modifiers() == QtCore.Qt.ShiftModifier: + if angle > 0: + self.changeOpacity(+0.1) + else: + self.changeOpacity(-0.1) + event.accept() + return True + # don't accept the event, so that others can accept it + return False + + +def main(): + app = QtWidgets.QApplication(sys.argv) + if sys.platform.startswith('win'): + import ctypes + myappid = 'fabrylab.saenopy.master' # arbitrary string + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) + + window = DrawWindow() + window.show() + try: + import pyi_splash + + # Update the text on the splash screen + pyi_splash.update_text("PyInstaller is a great software!") + pyi_splash.update_text("Second time's a charm!") + + # Close the splash screen. It does not matter when the call + # to this function is made, the splash screen remains open until + # this function is called or the Python program is terminated. + pyi_splash.close() + except (ImportError, RuntimeError): + pass + + while True: + try: + res = app.exec_() + break + except Exception as err: + traceback.print_traceback(err) + QtWidgets.QMessageBox.critical(window, "Error", f"An Error occurred:\n{err}") + continue + sys.exit(res) + + +if __name__ == "__main__": + main() diff --git a/saenopy/gui/tfm2d/modules/load_measurement_dialog.py b/saenopy/gui/tfm2d/modules/load_measurement_dialog.py new file mode 100644 index 0000000..ffcb5f0 --- /dev/null +++ b/saenopy/gui/tfm2d/modules/load_measurement_dialog.py @@ -0,0 +1,317 @@ +import sys +from qtpy import QtCore, QtWidgets, QtGui +import time + +import saenopy +import saenopy.multigrid_helper +import saenopy.get_deformations +import saenopy.multigrid_helper +import saenopy.materials +from saenopy.gui.common import QtShortCuts +from saenopy.gui.common.stack_selector import StackSelector +from saenopy.gui.common.stack_selector_crop import StackSelectorCrop +from saenopy.gui.common.stack_preview import StackPreview + +from saenopy.examples import get_examples_2D + +class AddFilesDialog(QtWidgets.QDialog): + mode: str = None + mode_data: str = None + start_time: float = 0 + + def __init__(self, parent, settings): + super().__init__(parent) + self.setMinimumWidth(800) + self.setMinimumHeight(300) + self.setWindowTitle("Add Files") + with QtShortCuts.QVBoxLayout(self) as layout: + with QtShortCuts.QTabWidget(layout) as self.tabs: + with self.tabs.createTab("New Measurement") as self.tab: + with QtShortCuts.QHBoxLayout(): + with QtShortCuts.QVBoxLayout(): + label1 = QtWidgets.QLabel("brightfield").addToLayout() + self.stack_bf_input = QtShortCuts.QInputFilename(None, None, file_type="Image Files (*.tif)", settings=settings, + settings_key="2d/input0", allow_edit=True, existing=True) + with QtShortCuts.QVBoxLayout(): + label1 = QtWidgets.QLabel("reference").addToLayout() + self.stack_reference_input = QtShortCuts.QInputFilename(None, None, file_type="Image Files (*.tif)", settings=settings, + settings_key="2d/input1", allow_edit=True, existing=True) + with QtShortCuts.QVBoxLayout(): + label1 = QtWidgets.QLabel("deformed").addToLayout() + self.stack_data_input = QtShortCuts.QInputFilename(None, None, file_type="Image Files (*.tif)", settings=settings, + settings_key="2d/input2", allow_edit=True, existing=True) + self.pixel_size = QtShortCuts.QInputString(None, "pixel size", 0.201, settings=settings, settings_key="2d/pixel_size", allow_none=False, type=float) + QtShortCuts.current_layout.addStretch() + self.outputText = QtShortCuts.QInputFolder(None, "output", settings=settings, + settings_key="2d/wildcard", allow_edit=True) + with QtShortCuts.QHBoxLayout(): + # self.button_clear = QtShortCuts.QPushButton(None, "clear list", self.clear_files) + QtShortCuts.current_layout.addStretch() + self.button_addList00 = QtShortCuts.QPushButton(None, "cancel", self.reject) + + self.button_addList0 = QtShortCuts.QPushButton(None, "ok", self.accept_new) + + with self.tabs.createTab("Existing Measurement") as self.tab3: + self.outputText3 = QtShortCuts.QInputFilename(None, "output", settings=settings, + file_type="Results Files (*.saenopy2D)", + settings_key="2d/load_existing", + allow_edit=True, existing=True) + self.tab3.addStretch() + with QtShortCuts.QHBoxLayout() as layout3: + layout3.addStretch() + self.button_addList6 = QtShortCuts.QPushButton(None, "cancel", self.reject) + + self.button_addList5 = QtShortCuts.QPushButton(None, "ok", self.accept_existing) + + with self.tabs.createTab("Examples") as self.tab4: + examples = get_examples_2D() + self.example_buttons = [] + with QtShortCuts.QHBoxLayout() as lay: + for example_name, properties in examples.items(): + with QtShortCuts.QGroupBox(None, example_name) as group: + group[0].setMaximumWidth(240) + label1 = QtWidgets.QLabel(properties["desc"]).addToLayout() + label1.setWordWrap(True) + label = QtWidgets.QLabel().addToLayout() + pix = QtGui.QPixmap(str(properties["img"])) + pix = pix.scaledToWidth( + int(200 * QtGui.QGuiApplication.primaryScreen().logicalDotsPerInch() / 96), + QtCore.Qt.SmoothTransformation) + label.setPixmap(pix) + label.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + self.button_example1 = QtShortCuts.QPushButton(None, "Open", + lambda *, example_name=example_name: self.load_example(example_name)) + self.example_buttons.append(self.button_example1) + self.button_example2 = QtShortCuts.QPushButton(None, "Open (evaluated)", + lambda *, example_name=example_name: self.load_example( + example_name, evaluated=True)) + self.button_example2.setEnabled(properties.get("url_evaluated", None) is not None) + self.example_buttons.append(self.button_example2) + lay.addStretch() + + self.tab4.addStretch() + self.download_state = QtWidgets.QLabel("").addToLayout() + self.download_progress = QtWidgets.QProgressBar().addToLayout() + self.download_progress.setRange(0, 100) + + def accept_new(self): + self.mode = "new" + self.accept() + + def accept_existing(self): + self.mode = "existing" + self.accept() + + def load_example(self, example_name, evaluated=False): + self.examples_output = saenopy.load_example(example_name, None, self.reporthook, evaluated=evaluated) + if evaluated: + self.mode = "example_evaluated" + else: + self.mode = "example" + self.mode_data = example_name + self.accept() + + def reporthook(self, count, block_size, total_size, msg=None): + if msg is not None: + print(msg) + self.download_state.setText(msg) + return + if count == 0: + self.start_time = time.time() + return + if total_size == -1: + return + duration = time.time() - self.start_time + progress_size = int(count * block_size) + speed = int(progress_size / (1024 * duration + 0.001)) + percent = int(count * block_size * 100 / total_size) + sys.stdout.write("\r...%d%%, %d MB, %d KB/s, %d seconds passed" % + (percent, progress_size / (1024 * 1024), speed, duration)) + sys.stdout.flush() + self.download_state.setText("...%d%%, %d MB, %d KB/s, %d seconds passed" % + (percent, progress_size / (1024 * 1024), speed, duration)) + self.download_progress.setValue(percent) + +class AddFilesDialogX(QtWidgets.QDialog): + mode: str = None + mode_data: str = None + start_time: float = 0 + + def __init__(self, parent, settings): + super().__init__(parent) + self.setMinimumWidth(800) + self.setMinimumHeight(600) + self.setWindowTitle("Add Files") + with QtShortCuts.QVBoxLayout(self) as layout: + with QtShortCuts.QTabWidget(layout) as self.tabs: + with self.tabs.createTab("New Measurement") as self.tab: + with QtShortCuts.QHBoxLayout(): + with QtShortCuts.QVBoxLayout(): + with QtShortCuts.QHBoxLayout(): + self.reference_choice = QtShortCuts.QInputChoice(None, "Reference", 0, [0, 1], + ["difference between time points", + "single stack"]) + QtShortCuts.current_layout.addStretch() + with QtShortCuts.QHBoxLayout(): + with QtShortCuts.QHBoxLayout(): + with QtShortCuts.QVBoxLayout(): + QtShortCuts.current_layout.setContentsMargins(0, 0, 0, 0) + self.place_holder_widget = QtWidgets.QWidget().addToLayout() + layout_place_holder = QtWidgets.QVBoxLayout(self.place_holder_widget) + layout_place_holder.addStretch() + + def ref_changed(): + self.stack_reference.setVisible(self.reference_choice.value()) + self.place_holder_widget.setVisible(not self.reference_choice.value()) + + self.reference_choice.valueChanged.connect(ref_changed) + self.stack_reference = StackSelector(QtShortCuts.current_layout, "reference") + self.stack_reference.glob_string_changed.connect \ + (lambda x, y: self.stack_reference_input.setText(y)) + self.stack_reference.setVisible(self.reference_choice.value()) + + self.stack_reference_input = QtWidgets.QLineEdit().addToLayout() + with QtShortCuts.QVBoxLayout(): + QtShortCuts.current_layout.setContentsMargins(0, 0, 0, 0) + self.stack_data = StackSelector(QtShortCuts.current_layout, "active stack(s)", + self.stack_reference, use_time=True) + self.stack_data.setMinimumWidth(300) + self.stack_reference.setMinimumWidth(300) + self.place_holder_widget.setMinimumWidth(300) + self.stack_data.glob_string_changed.connect( + lambda x, y: self.stack_data_input.setText(y)) + self.stack_data_input = QtWidgets.QLineEdit().addToLayout() + self.stack_crop = StackSelectorCrop(self.stack_data, self.reference_choice, self.stack_reference).addToLayout() + self.stack_data.stack_crop = self.stack_crop + self.stack_preview = StackPreview(QtShortCuts.current_layout, self.reference_choice, + self.stack_reference, self.stack_data) + self.outputText = QtShortCuts.QInputFolder(None, "output", settings=settings, + settings_key="batch/wildcard2", allow_edit=True) + with QtShortCuts.QHBoxLayout(): + # self.button_clear = QtShortCuts.QPushButton(None, "clear list", self.clear_files) + QtShortCuts.current_layout.addStretch() + self.button_addList00 = QtShortCuts.QPushButton(None, "cancel", self.reject) + + self.button_addList0 = QtShortCuts.QPushButton(None, "ok", self.accept_new) + + with self.tabs.createTab("Existing Measurement") as self.tab3: + self.outputText3 = QtShortCuts.QInputFilename(None, "output", settings=settings, + file_type="Results Files (*.saenopy2D)", + settings_key="batch/wildcard_existing", + allow_edit=True, existing=True) + self.tab3.addStretch() + with QtShortCuts.QHBoxLayout() as layout3: + layout3.addStretch() + self.button_addList6 = QtShortCuts.QPushButton(None, "cancel", self.reject) + + self.button_addList5 = QtShortCuts.QPushButton(None, "ok", self.accept_existing) + + with self.tabs.createTab("Examples") as self.tab4: + examples = get_examples() + self.example_buttons = [] + with QtShortCuts.QHBoxLayout() as lay: + for example_name, properties in examples.items(): + with QtShortCuts.QGroupBox(None, example_name) as group: + group[0].setMaximumWidth(240) + label1 = QtWidgets.QLabel(properties["desc"]).addToLayout() + label1.setWordWrap(True) + label = QtWidgets.QLabel().addToLayout() + pix = QtGui.QPixmap(str(properties["img"])) + pix = pix.scaledToWidth( + int(200 * QtGui.QGuiApplication.primaryScreen().logicalDotsPerInch() / 96), + QtCore.Qt.SmoothTransformation) + label.setPixmap(pix) + label.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + self.button_example1 = QtShortCuts.QPushButton(None, "Open", + lambda *, example_name=example_name: self.load_example(example_name)) + self.example_buttons.append(self.button_example1) + self.button_example2 = QtShortCuts.QPushButton(None, "Open (evaluated)", + lambda *, example_name=example_name: self.load_example( + example_name, evaluated=True)) + self.button_example2.setEnabled(properties.get("url_evaluated", None) is not None) + self.example_buttons.append(self.button_example2) + lay.addStretch() + + self.tab4.addStretch() + self.download_state = QtWidgets.QLabel("").addToLayout() + self.download_progress = QtWidgets.QProgressBar().addToLayout() + self.download_progress.setRange(0, 100) + + def accept_new(self): + self.mode = "new" + if self.reference_choice.value() == 1 and self.stack_reference.active is None: + QtWidgets.QMessageBox.critical(self, "Deformation Detector", + "Provide a stack for the reference state.") + elif self.stack_data.active is None: + QtWidgets.QMessageBox.critical(self, "Deformation Detector", + "Provide a stack for the deformed state.") + elif self.stack_data.get_t_count() <= 1 and self.stack_reference.active is None: + QtWidgets.QMessageBox.critical(self, "Deformation Detector", + "Provide either a reference stack or a time sequence.") + elif not self.stack_crop.validator(): + QtWidgets.QMessageBox.critical(self, "Deformation Detector", + "Enter a valid voxel size.") + elif "{t}" in self.stack_data_input.text() and not self.stack_crop.validator_time(): + QtWidgets.QMessageBox.critical(self, "Deformation Detector", + "Enter a valid time delta.") + else: + self.accept() + + def accept_existing(self): + self.mode = "existing" + self.accept() + + def load_example(self, example_name, evaluated=False): + self.examples_output = saenopy.load_example(example_name, None, self.reporthook, evaluated=evaluated) + if evaluated: + self.mode = "example_evaluated" + else: + self.mode = "example" + self.mode_data = example_name + self.accept() + + def reporthook(self, count, block_size, total_size, msg=None): + if msg is not None: + print(msg) + self.download_state.setText(msg) + return + if count == 0: + self.start_time = time.time() + return + duration = time.time() - self.start_time + progress_size = int(count * block_size) + speed = int(progress_size / (1024 * duration + 0.001)) + percent = int(count * block_size * 100 / total_size) + sys.stdout.write("\r...%d%%, %d MB, %d KB/s, %d seconds passed" % + (percent, progress_size / (1024 * 1024), speed, duration)) + sys.stdout.flush() + self.download_state.setText("...%d%%, %d MB, %d KB/s, %d seconds passed" % + (percent, progress_size / (1024 * 1024), speed, duration)) + self.download_progress.setValue(percent) + + +class FileExistsDialog(QtWidgets.QDialog): + mode: str = None + + def __init__(self, parent, filename): + super().__init__(parent) + self.setWindowTitle("File Exists") + with QtShortCuts.QVBoxLayout(self): + self.label = QtShortCuts.SuperQLabel(f"A file with the name {filename} already exists.").addToLayout() + self.label.setWordWrap(True) + with QtShortCuts.QHBoxLayout(): + self.use_for_all = QtShortCuts.QInputBool(None, "remember decision for all files", False) + with QtShortCuts.QHBoxLayout(): + self.button_addList0 = QtShortCuts.QPushButton(None, "cancel", self.reject) + + self.button_addList1 = QtShortCuts.QPushButton(None, "overwrite", self.accept_overwrite) + + self.button_addList2 = QtShortCuts.QPushButton(None, "read", self.accept_read) + + def accept_overwrite(self): + self.mode = "overwrite" + self.accept() + + def accept_read(self): + self.mode = "read" + self.accept() diff --git a/saenopy/gui/tfm2d/modules/result.py b/saenopy/gui/tfm2d/modules/result.py new file mode 100644 index 0000000..dd3b4aa --- /dev/null +++ b/saenopy/gui/tfm2d/modules/result.py @@ -0,0 +1,189 @@ +import io +import matplotlib.pyplot as plt +from saenopy.saveable import Saveable +import numpy as np +from tifffile import imread +from pyTFM.plotting import show_quiver + + +class Result2D(Saveable): + __save_parameters__ = ['bf', 'input', 'reference_stack', 'output', 'pixel_size', 'u', 'v', 'mask_val', 'mask_std', + 'tx', 'ty', 'fx', 'fy', + 'shape', 'mask', + 'piv_parameters', 'force_parameters', + '___save_name__', '___save_version__'] + ___save_name__ = "Result2D" + ___save_version__ = "1.0" + + input: str = None + reference_stack: str = None + output: str = None + pixel_size: float = None + + u: np.ndarray = None + v: np.ndarray = None + mask_val: np.ndarray = None + mask_std: np.ndarray = None + + tx: np.ndarray = None + ty: np.ndarray = None + + fx: np.ndarray = None + fy: np.ndarray = None + + drift_parameters: dict = {} + piv_parameters: dict = {} + force_parameters: dict = {} + force_gen_parameters: dict = {} + stress_parameters: dict = {} + + shape: tuple = None + mask: np.ndarray = None + + im_displacement: np.ndarray = None + im_force: np.ndarray = None + im_tension: np.ndarray = None + + def __init__(self, output, bf, input, reference_stack, pixel_size, **kwargs): + self.bf = bf + self.input = input + self.reference_stack = reference_stack + self.pixel_size = pixel_size + self.output = output + + path_b = Path(self.input) + path_a = Path(self.reference_stack) + path_b = path_b.parent / (path_b.stem + "_corrected" + path_b.suffix) + path_a = path_a.parent / (path_a.stem + "_corrected" + path_a.suffix) + self.input_corrected = str(path_b) + self.reference_stack_corrected = str(path_a) + + self.state = False + + self.get_image(0) + + super().__init__(**kwargs) + + def get_image(self, index, corrected=True): + if index == 0: + if corrected: + try: + im = imread(self.input_corrected) + except FileNotFoundError: + im = imread(self.input) + else: + im = imread(self.input) + print(im.shape, self.input) + elif index == -1: + im = imread(self.bf) + else: + if corrected: + try: + im = imread(self.reference_stack_corrected) + except FileNotFoundError: + im = imread(self.reference_stack) + else: + im = imread(self.reference_stack) + print(im.shape, self.reference_stack) + if self.shape is None: + self.shape = im.shape + return im + + def get_deformation_field(self): + if self.im_displacement is None: + fig1, ax = show_quiver(self.u, self.v, cbar_str="deformations\n[pixels]") + self.im_displacement = fig_to_numpy(fig1, self.shape) + return self.im_displacement + + def get_force_field(self): + if self.im_force is None: + fig1, ax = show_quiver(self.tx, self.ty, cbar_str="tractions\n[Pa]") + self.im_force = fig_to_numpy(fig1, self.shape) + return self.im_force + + def save(self, file_name=None): + if file_name is None: + file_name = self.output + Path(self.output).parent.mkdir(exist_ok=True, parents=True) + super().save(file_name) + + +def fig_to_numpy(fig1, shape): + fig1.axes[0].set_position([0, 0, 1, 1]) + fig1.axes[1].set_position([1, 1, 0.1, 0.1]) + fig1.set_dpi(100) + fig1.set_size_inches(shape[1] / 100, shape[0] / 100) + with io.BytesIO() as buff: + plt.savefig(buff, format="png") + buff.seek(0) + return plt.imread(buff) + +import glob +from pathlib import Path +import os +def get_stacks2D(output_path, bf_stack, active_stack, reference_stack, pixel_size, + exist_overwrite_callback=None, + load_existing=False): + output_base = Path(bf_stack).parent + while "*" in str(output_base): + output_base = Path(output_base).parent + + bf_stack = sorted(glob.glob(str(bf_stack))) + output_path = str(output_path) + active_stack = sorted(glob.glob(str(active_stack))) + reference_stack = sorted(glob.glob(str(reference_stack))) + + if len(bf_stack) == 0: + raise ValueError("no bf image selected") + if len(active_stack) == 0: + raise ValueError("no active image selected") + if len(reference_stack) == 0: + raise ValueError("no reference image selected") + + if len(bf_stack) != len(active_stack): + raise ValueError(f"the number of bf images ({len(bf_stack)}) does not match the number of active images {len(active_stack)}") + if len(bf_stack) != len(reference_stack): + raise ValueError(f"the number of bf images ({len(bf_stack)}) does not match the number of reference images {len(reference_stack)}") + + results = [] + for i in range(len(bf_stack)): + im0 = bf_stack[i] + im1 = active_stack[i] + im2 = reference_stack[i] + + output = Path(output_path) / os.path.relpath(im0, output_base) + output = output.parent / output.stem + output = Path(str(output) + ".saenopy2D") + + if output.exists(): + if exist_overwrite_callback is not None: + mode = exist_overwrite_callback(output) + if mode == 0: + break + if mode == "read": + data = Result2D.load(output) + data.is_read = True + results.append(data) + continue + elif load_existing is True: + data = Result2D.load(output) + data.is_read = True + results.append(data) + continue + + print("output", output) + print("im0", im0) + print("im1", im1) + print("im2", im2) + print("pixel_size", pixel_size) + data = Result2D( + output=str(output), + bf=str(im0), + input=str(im1), + reference_stack=str(im2), + pixel_size=float(pixel_size), + ) + data.save() + results.append(data) + + return results diff --git a/saenopy/gui/tmp/render_results2.py b/saenopy/gui/tmp/render_results2.py new file mode 100644 index 0000000..85495d5 --- /dev/null +++ b/saenopy/gui/tmp/render_results2.py @@ -0,0 +1,159 @@ +import numpy as np + +import saenopy +from pathlib import Path +import matplotlib.pyplot as plt + +params = {'image': {'width': 768, 'height': 768, 'logo_size': 0, 'scale': 1.0, 'antialiase': True}, 'camera': {'elevation': 26.52, 'azimuth': 39.0, 'distance': 1221, 'offset_x': 13, 'offset_y': 77, 'roll': 0}, 'theme': 'document', 'show_grid': 2, 'use_nans': False, 'arrows': 'fitted forces', 'averaging_size': 10.0, 'deformation_arrows': {'autoscale': True, 'scale_max': 10.0, 'colormap': 'turbo', 'arrow_scale': 1.0, 'arrow_opacity': 1.0, 'skip': 1}, 'force_arrows': {'autoscale': False, 'scale_max': 1000.0, 'use_center': False, 'colormap': 'turbo', 'arrow_scale': 1.0, 'arrow_opacity': 1.0, 'skip': 1}, 'stack': {'image': 2, 'channel': '01', 'z_proj': 2, 'use_contrast_enhance': True, 'contrast_enhance': (4.0, 29.0), 'colormap': 'gray', 'z': 188, 'use_reference_stack': False, 'channel_B': '', 'colormap_B': 'gray'}, 'scalebar': {'length': 0.0, 'width': 5.0, 'xpos': 15.0, 'ypos': 10.0, 'fontsize': 18.0}, '2D_arrows': {'width': 2.0, 'headlength': 5.0, 'headheight': 5.0}, 'crop': {'x': (156, 356), 'y': (156, 356), 'z': (163, 213)}, 'channel0': {'show': False, 'skip': 1, 'sigma_sato': 2, 'sigma_gauss': 0, 'percentiles': (0, 1), 'range': (0, 1), 'alpha': (0.1, 0.5, 1), 'cmap': 'pink'}, 'channel1': {'show': False, 'skip': 1, 'sigma_sato': 0, 'sigma_gauss': 7, 'percentiles': (0, 1), 'range': (0, 1), 'alpha': (0.1, 0.5, 1), 'cmap': 'Greens', 'channel': 1}, 'channel_thresh': 1.0, 'time': {'t': 0, 'format': '%d:%H:%M', 'start': 0.0, 'display': True, 'fontsize': 18}} +params = {'image': {'width': 768, 'height': 768, 'logo_size': 0, 'scale': 1.0, 'antialiase': True}, + 'camera': {'elevation': 16.7, 'azimuth': 31.58, 'distance': 1201, 'offset_x': 7, 'offset_y': 78, 'roll': 0}, + 'theme': 'document', 'show_grid': 3, 'use_nans': False, 'arrows': 'fitted forces', 'averaging_size': 10.0, + 'deformation_arrows': {'autoscale': True, 'scale_max': 10.0, 'colormap': 'turbo', 'arrow_scale': 1.0, 'arrow_opacity': 1.0, 'skip': 1}, + 'force_arrows': {'autoscale': False, 'use_log': True, 'scale_max': 80000.0, 'use_center': False, 'colormap': 'turbo', 'arrow_scale': 1.0, 'arrow_opacity': 1.0, 'skip': 1}, 'stack': {'image': 2, 'channel': '01', 'z_proj': 3, 'use_contrast_enhance': True, 'contrast_enhance': (4.0, 12.0), 'colormap': 'gray', 'z': 188, 'use_reference_stack': False, 'channel_B': '', 'colormap_B': 'gray'}, 'scalebar': {'length': 0.0, 'width': 5.0, 'xpos': 15.0, 'ypos': 10.0, 'fontsize': 18.0}, '2D_arrows': {'width': 2.0, 'headlength': 5.0, 'headheight': 5.0}, 'crop': {'x': (156, 356), 'y': (156, 356), 'z': (163, 213)}, 'channel0': {'show': False, 'skip': 1, 'sigma_sato': 2, 'sigma_gauss': 0, 'percentiles': (0, 1), 'range': (0, 1), 'alpha': (0.1, 0.5, 1), 'cmap': 'pink'}, 'channel1': {'show': False, 'skip': 1, 'sigma_sato': 0, 'sigma_gauss': 0, 'percentiles': (0, 1), 'range': (0, 1), 'alpha': (0.028954886793518153, 0.5444943820224719, 0.6402247191011237), 'cmap': 'Greens', 'channel': 1}, 'channel_thresh': 1.0, 'time': {'t': 0, 'format': '%d:%H:%M', 'start': 0.0, 'display': True, 'fontsize': 18}} +folder = Path("/home/richard/.local/share/saenopy/1_ClassicSingleCellTFM/example_output") + +if 1: + j = [] + pos = "007" + for use_log in [False, True]: # "004", "007", + params["force_arrows"]["use_log"] = use_log + im = saenopy.render_image(params, saenopy.load(folder / f"Pos{pos}_S001_z{{z}}_ch{{c00}}_a-2.saenopy")) + + #im2 = render_image(params, saenopy.load(folder / f"Pos{pos}_S001_z{{z}}_ch{{c00}}_new14_boundary-False_saenonew.saenopy")) + + im0 = saenopy.render_image(params, saenopy.load(folder / f"Pos{pos}_S001_z{{z}}_ch{{c00}}_new14_boundary-False_saenooldbulk.saenopy")) + + #im00 = render_image(params, saenopy.load(folder / f"Pos{pos}_S001_z{{z}}_ch{{c00}}_new14_boundary-True_saenonew_strong.saenopy")) + + #j.append(np.hstack([im0, im, im2, im00])) + j.append(np.hstack([im, im0])) + plt.imsave("test2.png", np.vstack(j)) + exit() +if 1: + pos = "004" + import sys + sys.path.insert(0, "/home/richard/PycharmProjects/utils") + from rgerum_utils.plot.plot_group_new import PlotGroup + + group = PlotGroup() + for pos in group.row(["007"]): #["004", "007", "008"] + res0 = saenopy.load(folder / f"Pos{pos}_S001_z{{z}}_ch{{c00}}_new14_boundary-False_saenooldbulk.saenopy") + res1 = saenopy.load(folder / f"Pos{pos}_S001_z{{z}}_ch{{c00}}_a-2.saenopy") + #res2 = saenopy.load(folder / f"Pos{pos}_S001_z{{z}}_ch{{c00}}_new14_boundary-False_saenonew.saenopy") + #res3 = saenopy.load(folder / f"Pos{pos}_S001_z{{z}}_ch{{c00}}_new14_boundary-True_saenonew_strong2.saenopy") + + def get_cell_boundary(result: saenopy.Result, channel=1, thershold=20, smooth=2, element_size=14.00e-6, boundary=True, pos=None, label=None): + from scipy.ndimage import gaussian_filter + import matplotlib.pyplot as plt + import numpy as np + + for i in range(len(result.stacks)): + if 0: + mesh = result.solvers[i].mesh + index = np.argsort(mesh.nodes[:, 1]) + x = mesh.nodes[index, 1] + f = np.linalg.norm(mesh.forces[index], axis=1)*mesh.regularisation_mask + xl = [] + fl = [] + for xx in np.unique(x): + xl.append(xx) + fl.append(np.max(f[x == xx])) + count = np.sum(x == x[0]) + print(count) + plt.plot(xl, fl, "-", label=label) + return + + stack_deformed = result.stacks[i] + voxel_size1 = stack_deformed.voxel_size + + im = stack_deformed[:, :, 0, :, channel] + im = gaussian_filter(im, sigma=smooth, truncate=2.0) + + im_thresh = (im[:, :, :] > thershold).astype(np.uint8) + + + from skimage.measure import label + def largest_connected_component(segmentation): + labels = label(segmentation) + counts = [0] + for i in range(1, np.max(labels)): + counts.append(np.sum(i == labels)) + return labels == np.argmax(counts) + largest_cc = labels == np.argmax(np.bincount(labels[segmentation])) + return largest_cc + + print(im_thresh.shape, im_thresh.dtype) + im_thresh = largest_connected_component(im_thresh).astype(np.uint8) + print(im_thresh.shape,im_thresh.dtype) + + from skimage.morphology import erosion + if boundary: + im_thresh = (im_thresh - erosion(im_thresh)).astype(bool) + else: + im_thresh = im_thresh.astype(bool) + #if pos == "004": + # im_thresh[:, :, :112] = False + du, dv, dw = voxel_size1 + + u = im_thresh + y, x, z = np.indices(u.shape) + y, x, z = (y * stack_deformed.shape[0] * dv / u.shape[0] * 1e-6, + x * stack_deformed.shape[1] * du / u.shape[1] * 1e-6, + z * stack_deformed.shape[2] * dw / u.shape[2] * 1e-6) + z -= np.max(z) / 2 + x -= np.max(x) / 2 + y -= np.max(y) / 2 + + x = x[im_thresh] + y = y[im_thresh] + z = z[im_thresh] + + yxz = np.vstack([y, x, z]) + + difference_vec = result.solvers[0].mesh.nodes[:, :, None] - yxz[None, :, :] + difference_length = np.linalg.norm(difference_vec, axis=1) + index = np.argmin(difference_length, axis=1) + print(difference_vec.shape) + print(difference_length.shape) + print(result.solvers[0].mesh.nodes.shape) + print(index.shape, index.dtype) + #difference_vec = difference_vec[index] + difference_vec = np.array([difference_vec[i, :, x] for i, x in enumerate(index)]) + print(difference_vec.shape) + dist_to_cell = np.min(difference_length, axis=1) + + difference_vec_normalized = difference_vec / dist_to_cell[:, None] + + print(dist_to_cell.shape) + print(result.solvers[0].mesh.forces.shape) + x = dist_to_cell.ravel() + y = np.linalg.norm(result.solvers[0].mesh.forces * result.solvers[0].mesh.regularisation_mask[:, None], axis=1) + y_proj = np.sum(result.solvers[0].mesh.forces * result.solvers[0].mesh.regularisation_mask[:, None] * difference_vec_normalized, axis=1) + i = np.argsort(x) + x = x[i] + y = y[i] + y_proj = y_proj[i] + y = np.cumsum(y[::-1])[::-1] + #y_proj = np.cumsum(y_proj[::-1])[::-1] + + #group.select_ax(col=0) + y = y/y[0] + l, = plt.plot(x*1e6, y*100, "-", label=label) + index_max = np.where(y<0.05)[0][0] + print(x[index_max]*1e6) + plt.axvline(x[index_max]*1e6, color=l.get_color(), lw=0.8, linestyle='--') + return l + #group.select_ax(col=1) + #y_proj = y_proj/y_proj[0] + #plt.plot(x*1e6, y_proj*100, "--", label=label, color=l.get_color()) + + l1 = get_cell_boundary(res0, label="saeno") + l2 = get_cell_boundary(res1, label="saenopy") + #get_cell_boundary(res2, label="surface") + #get_cell_boundary(res3, label="surface2") + plt.axhline(5, color='k', linestyle='--', lw=.8) + plt.xlabel("Distance to Cell Surface (µm)") + plt.ylabel("Percentage of Forces (%)") + plt.legend([l1, l2], ["Saeno", "Saenopy"], frameon=False) + plt.savefig("surface_comparison.png") + plt.show() \ No newline at end of file diff --git a/saenopy/gui/tmp/render_surface_comparison.py b/saenopy/gui/tmp/render_surface_comparison.py new file mode 100644 index 0000000..921a9b4 --- /dev/null +++ b/saenopy/gui/tmp/render_surface_comparison.py @@ -0,0 +1,86 @@ +import saenopy +from pathlib import Path +import matplotlib.pyplot as plt + +folder = Path("/home/richard/.local/share/saenopy/1_ClassicSingleCellTFM/example_output") + +pos = "007" +res0 = saenopy.load(folder / f"Pos{pos}_S001_z{{z}}_ch{{c00}}_new14_boundary-False_saenooldbulk.saenopy") +res1 = saenopy.load(folder / f"Pos{pos}_S001_z{{z}}_ch{{c00}}_a-2.saenopy") + +def get_cell_boundary(result: saenopy.Result, channel=1, thershold=20, smooth=2, element_size=14.00e-6, boundary=True, pos=None, label=None): + from scipy.ndimage import gaussian_filter + import matplotlib.pyplot as plt + import numpy as np + + for i in range(len(result.stacks)): + stack_deformed = result.stacks[i] + voxel_size1 = stack_deformed.voxel_size + + im = stack_deformed[:, :, 0, :, channel] + im = gaussian_filter(im, sigma=smooth, truncate=2.0) + + im_thresh = (im[:, :, :] > thershold).astype(np.uint8) + + + from skimage.measure import label + def largest_connected_component(segmentation): + labels = label(segmentation) + counts = [0] + for i in range(1, np.max(labels)): + counts.append(np.sum(i == labels)) + return labels == np.argmax(counts) + + im_thresh = largest_connected_component(im_thresh).astype(np.uint8) + + from skimage.morphology import erosion + if boundary: + im_thresh = (im_thresh - erosion(im_thresh)).astype(bool) + else: + im_thresh = im_thresh.astype(bool) + + du, dv, dw = voxel_size1 + + u = im_thresh + y, x, z = np.indices(u.shape) + y, x, z = (y * stack_deformed.shape[0] * dv / u.shape[0] * 1e-6, + x * stack_deformed.shape[1] * du / u.shape[1] * 1e-6, + z * stack_deformed.shape[2] * dw / u.shape[2] * 1e-6) + z -= np.max(z) / 2 + x -= np.max(x) / 2 + y -= np.max(y) / 2 + + x = x[im_thresh] + y = y[im_thresh] + z = z[im_thresh] + + yxz = np.vstack([y, x, z]) + + difference_vec = result.solvers[0].mesh.nodes[:, :, None] - yxz[None, :, :] + difference_length = np.linalg.norm(difference_vec, axis=1) + dist_to_cell = np.min(difference_length, axis=1) + + x = dist_to_cell.ravel() + y = np.linalg.norm(result.solvers[0].mesh.forces * result.solvers[0].mesh.regularisation_mask[:, None], axis=1) + i = np.argsort(x) + x = x[i] + y = y[i] + + y = np.cumsum(y[::-1])[::-1] + + y = y/y[0] + l, = plt.plot(x*1e6, y*100, "-", label=label) + index_max = np.where(y<0.05)[0][0] + print(x[index_max]*1e6) + plt.axvline(x[index_max]*1e6, color=l.get_color(), lw=0.8, linestyle='--') + return l + +l1 = get_cell_boundary(res0, label="saeno") +l2 = get_cell_boundary(res1, label="saenopy") + +plt.axhline(5, color='k', linestyle='--', lw=.8) +plt.xlabel("Distance to Cell Surface (µm)") +plt.ylabel("Percentage of Forces (%)") +plt.legend([l1, l2], ["Saeno", "Saenopy"], frameon=False) +plt.savefig("surface_comparison.png") +plt.show() \ No newline at end of file diff --git a/saenopy/saveable.py b/saenopy/saveable.py index 4973b02..2f0738a 100644 --- a/saenopy/saveable.py +++ b/saenopy/saveable.py @@ -41,11 +41,11 @@ def save(self, filename: str, file_format=None): if file_format == ".h5py" or file_format == ".h5": # pragma: no cover return dict_to_h5(filename, flatten_dict(data)) - elif file_format == ".npz" or file_format == ".saenopy": + elif file_format == ".npz" or file_format == ".saenopy" or file_format == ".saenopy2D": #np.savez(filename, **data) np.lib.npyio._savez(filename, [], flatten_dict(data), True, allow_pickle=False) import shutil - if file_format == ".saenopy": + if file_format == ".saenopy" or file_format == ".saenopy2D": shutil.move(filename+".npz", filename) else: raise ValueError("format not supported") @@ -87,7 +87,7 @@ def load(cls, filename, file_format=None): import h5py data = h5py.File(filename, "a") result = cls.from_dict(unflatten_dict_h5(data)) - elif file_format == ".npz" or file_format == ".saenopy": + elif file_format == ".npz" or file_format == ".saenopy" or file_format == ".saenopy2D": data = np.load(filename, allow_pickle=False) result = cls.from_dict(unflatten_dict(data)) diff --git a/saenopy/solver.py b/saenopy/solver.py index 513a8c4..5f831a5 100644 --- a/saenopy/solver.py +++ b/saenopy/solver.py @@ -640,7 +640,7 @@ def _update_local_regularization_weigth(self, method: str): self.localweight[index & self.mesh.movable] = 1e-10 if self.mesh.cell_boundary_mask is not None: - self.localweight[:] = 0.03 + self.localweight[:] = 0.03*100 self.localweight[self.mesh.cell_boundary_mask] = 0.003*0.001 self.localweight[~self.mesh.regularisation_mask] = 0