Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5fa2419
added corrector class, edited state streaming_save and gui
Mar 2, 2020
1e9b1b7
reference acquisition
Mar 3, 2020
a33e0f5
added registration and correction part
Mar 4, 2020
8706805
same changes
Mar 5, 2020
87c5c09
gui part ok
Mar 9, 2020
30f83cd
motors in separate processes
Mar 9, 2020
a85270a
problems with pyvisa and multiprocessing
Mar 9, 2020
08a4ff7
motors in gui update
Mar 9, 2020
700932a
motors in gui update
Mar 9, 2020
988c33f
motors ok, working on information streaming
Mar 9, 2020
6756dcd
working on communications
Mar 10, 2020
134b588
motors again in a (single) separate process but connection establishe…
EmanPaoli Apr 7, 2020
2f0801e
reference sent to the corrector as a single array, introduced a gauss…
EmanPaoli Apr 7, 2020
7a9e54b
added simulated_tasks, preview works remotely
EmanPaoli Apr 16, 2020
e5a626f
fixes in corrector class
EmanPaoli Apr 26, 2020
efc24e5
fixes in corrector class
EmanPaoli May 6, 2020
c20aea3
reference received by the corrector
May 6, 2020
de04e4e
working single plane reference acquisition
May 6, 2020
7432f2d
merge of quarantine branch into copy of the master
EmanPaoli Jun 30, 2020
a786341
add ref parts to stacksaver
diegoasua Jun 30, 2020
9e6e3de
complete plane and fill reference fun in stacksaver
diegoasua Jun 30, 2020
8f335d4
calculate_fov in corrector
diegoasua Jun 30, 2020
00d5bd5
calibration vector in corrector
diegoasua Jun 30, 2020
b47fb06
reference stuff in scanner
diegoasua Jun 30, 2020
a7979c9
bug fix
diegoasua Jun 30, 2020
125baba
delete sim tasks
diegoasua Jun 30, 2020
2dcfa26
delete sim tasks
diegoasua Jun 30, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 251 additions & 0 deletions twop/drift_correction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
from multiprocessing import Process, Queue
import numpy as np
from lightparam.param_qt import ParametrizedQt
from lightparam import Param
from skimage.feature import register_translation
import flammkuchen as fl
from matplotlib import Path
from queue import Empty
from dataclasses import dataclass
from time import sleep
from scipy.ndimage.filters import gaussian_filter
from twop.objective_motor_sliders import MovementType
from scipy.signal import convolve


class ReferenceSettings(ParametrizedQt):
def __init__(self):
super().__init__()
self.name = "reference"
self.n_frames_ref = Param(10, (1, 500))
self.extra_planes = Param(11, (1, 500))
self.dz = Param(1.0, (0.1, 20.0), unit="um")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't there already a parameter like this in status? Don't duplicate parameters, move them through queues

self.xy_th = Param(5.0, (0.1, 20.0), unit="um")
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the _th are thresholds? please use longer names

self.z_th = Param(self.dz, (self.dz, self.dz * 4), unit="um")
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might have problems with parameters that depend on other parameters (I think here the values will just be copied)

self.n_frames_exp = Param(5, (1, 500))
self.size_k = Param(0, (0, 100))
self.sigma_k = Param(2, (1, 10))



@dataclass
class ReferenceParameters:
n_frames_ref: int = 10
extra_planes: int = 10
dz: float = 1.0
xy_th: float = 5
z_th: float = 1
n_frames_exp: int = 5
size_k: int = 0
sigma_k: int = 5
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is k?



def convert_reference_params(st: ReferenceSettings) -> ReferenceParameters:
n_frames_ref = st.n_frames_ref
extra_planes = st.extra_planes
xy_th = st.xy_th
dz = st.dz
z_th = st.z_th
n_frames_exp = st.n_frames_exp
sigma_k = st.sigma_k
size_k = st.size_k
rp = ReferenceParameters(n_frames_ref=n_frames_ref,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if all the names are same, you can covert settings to a dictionary (provided by lighparam). Also we should add a possibility to make a dataclass out of a Parameterized

extra_planes=extra_planes,
dz=dz,
xy_th=xy_th,
z_th=z_th,
n_frames_exp=n_frames_exp,
sigma_k=sigma_k,
size_k=size_k)

return rp


class Corrector(Process):
def __init__(self, reference_event, experiment_start_event, stop_event, correction_event,
reference_queue, scanning_parameters,
scanning_parameters_queue, data_queue,
input_commands_queues, output_positions_queues, save_param):
super().__init__()
# communication with other processes, active during acquisition of the reference
self.reference_event = reference_event
# communication with other processes, active during acquisition of the reference and experiment
# (all the processes use it)
self.experiment_start_event = experiment_start_event
# communication with other processes, the status is not modified by any process
self.stop_event = stop_event
# communication with other processes, active during experiment (when correction is allowed)
self.correction_event = correction_event

# queue for the acquisition of the reference, planes are sent by the saver
self.reference_queue = reference_queue
# queue in order to know the latest settings selected by the user such as n_planes, n_frames etc.
self.reference_param_queue = Queue()
# queue in order to know the latest scanning settings, for n_x and n_y
self.scanning_parameters_queue = scanning_parameters_queue
# initial scanning parameters
self.scanning_parameters = scanning_parameters
# queue for getting the copy of the frames during an experiment
self.data_queue = data_queue
# queue for the communication with the master motor class (in a separate process), this is for move the motors
self.input_commands_queues = input_commands_queues
# queue for the communication with the master motor class (in a separate process),
# this is for read the last position
self.output_positions_queues = output_positions_queues
# to know the directory where the anatomy/reference will be saved
self.save_parameters = save_param

self.x_pos = None
self.y_pos = None
self.z_pos = None
self.mov_type = MovementType(False)

self.reference = None
self.reference_params = None
self.calibration_vector = None

def run(self):
while True:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be while not stop_event.is_set() otherwise this process won't close properly. Also make sure it is .join() in the experiment termination function

self.update_settings()
if self.reference_event.is_set() and self.experiment_start_event.is_set():
self.start_ref_acquisition()
self.reference_loop()
self.reference_event.clear()

elif not self.reference_event.is_set() and self.experiment_start_event.is_set():
self.correction_event.set()
self.exp_loop()
self.correction_event.clear()

def reference_loop(self):
stack_4d = self.get_next_entry(self.reference_queue)
self.save_reference(stack_4d)
self.reference = self.reference_processing(stack_4d)
print(self.reference.shape)
self.end_ref_acquisition()

def compute_registration(self, test_image):
vectors = []
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can preallocate this as a numpy array, the length is known in advance

errors = []
planes = np.size(self.reference, 0)
for i in range(planes):
ref_im = np.squeeze(self.reference[i, :, :])
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please, no squeezing

output = register_translation(ref_im, test_image)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once you upgrade scikit-image to 0.17 the function has been moved

vectors.append(output[0])
errors.append(output[1])
ind = errors.index(min(errors))
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use np.argmin

z_disp = ind - ((self.reference_params.n_planes - 1) / 2)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is happening here?

vector = vectors[ind]
np.append(vector, z_disp)
vector = self.real_units(vector)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a more concise way of doing thse would be self.to_real_units(np.r_[vector, z_disp]) without the np.append np.r_ is a shortcut to concatenate

return vector

def exp_loop(self):
pix_millimeter = self.calculate_fov()
self.calibration_vector = [pix_millimeter, pix_millimeter, self.reference_params.dz] # x,y,z cal vect
while not self.stop_event.is_set():
number_of_frames = 0
frame_container = []
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know the size of the frame container? If so better use a preallocated numpy then index it

while number_of_frames == self.reference_params.n_frames_exp:
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a bit strange condition? I aussume it will never be fullfilled unless self.reference_params.n_frames_exp is 0, because the loop does not update the self.reference_params

try:
frame = self.data_queue.get(timeout=0.001)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you get from data_queue does the Saver still get the same data? You have to have into account that the get() method in a multiprocessing.queue not only obtains an element of the queue but also deletes it from the queue.

frame_container.append(frame)
number_of_frames += 1
except Empty:
frame_container = frame_container[-self.reference_params.n_frames_exp:]
frame = self.frame_processing(frame_container)
vector = self.compute_registration(frame)
self.apply_correction(vector)

def start_ref_acquisition(self):
self.x_pos = self.get_last_entry(self.output_positions_queues["x"])
self.y_pos = self.get_last_entry(self.output_positions_queues["y"])
self.z_pos = self.get_last_entry(self.output_positions_queues["z"])
# self.reference_params.n_planes = self.reference_params.n_planes + 1
up_planes = self.reference_params.extra_planes
distance = (self.reference_params.dz / 1000) * up_planes
self.input_commands_queues["z"].put((distance, self.mov_type))
sleep(0.2)

def end_ref_acquisition(self):
self.input_commands_queues["z"].put((self.z_pos, self.mov_type))

def real_units(self, raw_vector):
vector = np.multiply(raw_vector, self.calibration_vector)
return vector

def apply_correction(self, vector):
self.input_commands_queues["x"].put((vector[1], self.mov_type))
self.input_commands_queues["y"].put((vector[0], self.mov_type))
self.input_commands_queues["z"].put((vector[2], self.mov_type))

def reference_processing(self, input_ref):
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A tip for speed: once you received the reference, you can immediately fourier transform it and low-pass filter it in fourier space. That way the register_translation will be much faster (see the space argument here https://scikit-image.org/docs/stable/api/skimage.registration.html?highlight=phase_cross_correlation#phase-cross-correlation and the source code of the function)

output_ref = np.mean(input_ref, axis=0)
size_kernel = self.reference_params.size_k
sigma = self.reference_params.sigma_k
if size_kernel != 0:
kernel = self.gaussian_kernel((size_kernel,)*3, sigma=sigma)
output_ref = convolve(output_ref, kernel, mode='same')
return output_ref

@staticmethod
def frame_processing(frame_container):
frame_container_array = np.array(frame_container)
frame = np.mean(frame_container_array, 0)
return frame

@staticmethod
def get_last_entry(queue):
out = None
while True:
try:
out = queue.get(timeout=0.001)
except Empty:
break
return out

@staticmethod
def get_next_entry(queue):
out = None
while out is None:
try:
out = queue.get(timeout=0.001)
except Empty:
pass
return out
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This while will freeze the process here if nothing is received for whatever reason


@staticmethod
def gaussian_kernel(size_kernel, sigma=1):
size_kernel = np.ceil(size_kernel) // 2 * 2 + 1
x = np.arange(- np.floor(size_kernel[0] / 2), np.ceil(size_kernel[0] / 2), 1)
y = np.arange(- np.floor(size_kernel[1] / 2), np.ceil(size_kernel[1] / 2), 1)
if len(size_kernel) == 3:
z = np.arange(- np.floor(size_kernel[2] / 2), np.ceil(size_kernel[2] / 2), 1)
xx, yy, zz = np.meshgrid(x, y, z)
kernel = np.exp(-(xx ** 2 + yy ** 2 + zz ** 2) / (2 * sigma ** 2))
else:
xx, yy = np.meshgrid(x, y)
kernel = np.exp(-(xx ** 2 + yy ** 2) / (2 * sigma ** 2))
return kernel

def update_settings(self):
new_params = self.get_last_entry(self.scanning_parameters_queue)
if new_params is not None:
self.scanning_parameters = new_params

def calculate_fov(self):
# calculate pix per millimeters
# formula: width FOV (microns) = 167.789 * Voltage
conv_fact = 167.789
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conv_fact should not be declared here. Add it to a ParametrizedQt class here or in the state and set the attribute gui=False

w_fov = conv_fact * self.scanning_parameters.voltage_x
return (self.scanning_parameters.n_x / w_fov) / 1000

def save_reference(self, raw_reference):
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this class should be managing the saving of the reference. Try to constrain it just to do drift correction and nothing else.

n_planes = self.reference.shape[1]
for plane in range(n_planes):
fl.save(
Path(self.save_parameters.output_dir)
/ "anatomy/{:04d}.h5".format(plane),
{"stack_4D": raw_reference[:, plane, :, :]},
compression="blosc",
)
28 changes: 26 additions & 2 deletions twop/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
QFileDialog,
QCheckBox,
)
from state import ExperimentState, ScanningParameters, frame_duration
from twop.state import ExperimentState, ScanningParameters, frame_duration
from twop.objective_motor_sliders import MotionControlXYZ

import pyqtgraph as pg
Expand Down Expand Up @@ -51,6 +51,7 @@ def __init__(self, state: ExperimentState):
self.startstop_button = QPushButton()
self.set_saving()
self.chk_pause = QCheckBox("Pause after experiment")
self.chk_drift_corr = QCheckBox("Drift Correction")
self.stack_progress = QProgressBar()
self.plane_progress = QProgressBar()
self.plane_progress.setFormat("Frame %v of %m")
Expand All @@ -62,6 +63,7 @@ def __init__(self, state: ExperimentState):
self.layout().addWidget(self.save_location_button)
self.layout().addWidget(self.startstop_button)
self.layout().addWidget(self.chk_pause)
self.layout().addWidget(self.chk_drift_corr)
self.layout().addWidget(self.plane_progress)
self.layout().addWidget(self.stack_progress)

Expand All @@ -78,6 +80,11 @@ def set_notsaving(self):
)

def toggle_start(self):
if self.chk_drift_corr.isChecked() is True:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is True is redundant as the if statement is already evaluating isChecked() which returns only True or False

self.state.reference_event.set()
else:
self.state.reference_event.clear()

if self.state.saving:
self.state.end_experiment(force=True)
self.set_saving()
Expand All @@ -86,6 +93,8 @@ def toggle_start(self):
if self.state.start_experiment():
self.set_notsaving()



def set_locationbutton(self):
pathtext = self.state.experiment_settings.save_dir
# check if there is a stack in this location
Expand Down Expand Up @@ -187,6 +196,16 @@ def toggle_pause(self):
self.update_button()


class ReferenceWidget(QWidget):
def __init__(self, state: ExperimentState):
self.state = state
super().__init__()
self.reference_layout = QVBoxLayout()
self.reference_settings_gui = ParameterGui(self.state.reference_settings)
self.reference_layout.addWidget(self.reference_settings_gui)
self.setLayout(self.reference_layout)


class TwopViewer(QMainWindow):
def __init__(self):
super().__init__()
Expand All @@ -199,13 +218,18 @@ def __init__(self):

self.scanning_widget = ScanningWidget(self.state)
self.experiment_widget = ExperimentControl(self.state)
self.reference_widget = ReferenceWidget(self.state)

self.motor_control_slider = MotionControlXYZ(self.state.motors)
self.motor_control_slider = MotionControlXYZ(self.state.input_queues, self.state.output_queues)

self.addDockWidget(
Qt.LeftDockWidgetArea,
DockedWidget(widget=self.scanning_widget, title="Scanning settings"),
)
self.addDockWidget(
Qt.LeftDockWidgetArea,
DockedWidget(widget=self.reference_widget, title="Reference settings"),
)
self.addDockWidget(
Qt.RightDockWidgetArea,
DockedWidget(widget=self.motor_control_slider, title="Stage control"),
Expand Down
Loading