diff --git a/deepthought/comms.py b/deepthought/comms.py index 316c7a1..8092e9a 100644 --- a/deepthought/comms.py +++ b/deepthought/comms.py @@ -2,7 +2,19 @@ import rpyc from rpyc.utils.server import ThreadedServer + def server(object_, port, *args, **kwargs): + """serving an object in a port. + + parameters + -- + object_ : object + any python object that needs to be network enabled. + port : int + defines the port where the server is listening for requests. + + + """ s = ThreadedServer(object_, hostname="", port=port, auto_register=None, protocol_config={"allow_all_attrs": True, "allow_pickle" : True, @@ -12,6 +24,15 @@ def server(object_, port, *args, **kwargs): return s def client(addr, port, *args, **kwargs): + """generic function to connect to a rpyc server. + + parameters + -- + addr : str + ip address/url of the server + port : int + port + """ obj = rpyc.connect(addr, port, config={ "allow_all_attrs": True, "allow_pickle" : True }) diff --git a/deepthought/configs.py b/deepthought/configs.py index 9a989df..181a47c 100644 --- a/deepthought/configs.py +++ b/deepthought/configs.py @@ -1,11 +1,13 @@ -from collections import OrderedDict +from collections import OrderedDict import os config = OrderedDict() + def abspath(path): return os.path.abspath(path) + def read_env_value(name, default): if name in os.environ: value = os.environ[name] @@ -13,14 +15,21 @@ def read_env_value(name, default): value = default return value + MM_DIR = { - "name" : "MM_DIR", - "default" : "C:\Program Files\Micro-Manager-2.0gamma" + "name": "MM_DIR", + "default": "C:\Program Files\Micro-Manager-2.0gamma" } MM_CONFIG = { - "name" : "MM_CONFIG", - "default" : "./mmconfigs/Bright_Star.cfg" + "name": "MM_CONFIG", + "default": "./mmconfigs/Bright_Star.cfg" +} +MM_SERVER = { + "name": "MM_SERVER", + "default": "localhost", } - config["mm_dir"] = abspath(read_env_value(**MM_DIR)) config["mm_config"] = abspath(read_env_value(**MM_CONFIG)) +config["mm_server"] = {"addr": read_env_value(**MM_SERVER), "port": 18861} +store_disk = True +# feature toggle diff --git a/deepthought/detection.py b/deepthought/detection.py index 12d9e14..2fba46f 100644 --- a/deepthought/detection.py +++ b/deepthought/detection.py @@ -1,3 +1,4 @@ +import numpy as np import tifffile from cellpose import models @@ -27,5 +28,10 @@ def detect_object(image, kind="dapi"): elif kind == "cyto": seg_func = segment_cyto + if image.shape[0] > 1: + labels_ = np.array([seg_func(img) for img in image]) + + return labels_ + label_ = seg_func(image) - return (image, label_) + return label_ diff --git a/deepthought/devices.py b/deepthought/devices.py index 1e3bd0b..4101e85 100644 --- a/deepthought/devices.py +++ b/deepthought/devices.py @@ -7,17 +7,34 @@ 2. https://github.com/SEBv15/GSD192-tools """ -import time import threading -from typing import Dict, List, Any, TypeVar, Tuple - +import time +from collections import OrderedDict +from typing import Any, Dict, List, Tuple, TypeVar -from collections import OrderedDict +from ophyd import Component +from ophyd import DynamicDeviceComponent as DDCpt +from ophyd import PseudoPositioner +from ophyd import PseudoSingle +from ophyd import SoftPositioner +from ophyd.pseudopos import pseudo_position_argument +from ophyd.pseudopos import real_position_argument import numpy as np +from ophyd.status import Status +from ophyd.status import MoveStatus +from ophyd.mixins import SignalPositionerMixin +from ophyd import Signal + from skimage import io +import warnings +from comms import client + + +def get_mmc(): + mmc = client(addr="10.10.1.35", port=18861).mmc + return mmc -from ophyd.status import Status class BaseScope: def __init__(self, mmc): @@ -107,9 +124,18 @@ def random_crop(image, size=512): return cropped_image +def frame_crop(image, size=512, tol=100): + """generate a random crop of the image for the given size""" + error = np.random.randint(0, tol) + x, y = 150, 250 + cropped_image = image[x+error:x+size+error, y+error:y+size+error] + return cropped_image + + class SimMMC: """This is a simulated microscope that returns a 512x512 image.""" + def __init__(self): self.pos = 0 self.xy = [0, 0] @@ -136,17 +162,16 @@ def getPosition(self): def setXYPosition(self, value): self.xy = value return - + def getXYPosition(self): return self.xy - def waitForDevice(self, label): time.sleep(1) return def getImage(self): - return random_crop(self.data) + return frame_crop(self.data) class Focus: @@ -160,7 +185,7 @@ def __init__(self, mmc): def trigger(self): status = Status(obj=self, timeout=10) - + def wait(): try: self.mmc.waitForDevice(self.mmc_device_name) @@ -172,22 +197,22 @@ def wait(): threading.Thread(target=wait).start() return status - + def read(self): data = OrderedDict() data['z'] = {'value': self.mmc.getPosition(), 'timestamp': time.time()} - return data + return data def describe(self): data = OrderedDict() - data['z'] = {'source': "MMCore", + data['z'] = {'source': "MMCore", 'dtype': "number", - 'shape' : []} - return data - + 'shape': []} + return data def set(self, value): status = Status(obj=self, timeout=5) + def wait(): try: self.mmc.setPosition(float(value)) @@ -212,13 +237,14 @@ def describe_configuration(self) -> OrderedDict: class Camera: name = "camera" parent = None + exposure_time = None def __init__(self, mmc): self.mmc = mmc self.mmc_device_name = str(self.mmc.getCameraDevice()) self.image = None - + self.configure() self._subscribers = [] def _collection_callback(self): @@ -227,7 +253,7 @@ def _collection_callback(self): def trigger(self): status = Status(obj=self, timeout=10) - + def wait(): try: self.image_time = time.time() @@ -247,6 +273,23 @@ def wait(): return status + def configure(self): + cam_name = self.mmc.getCameraDevice() + + def configure_cam(prop, idx): + values = self.mmc.getAllowedPropertyValues(cam_name, prop) + self.mmc.setProperty(cam_name, prop, values[idx]) + return self.mmc.getProperty(cam_name, prop) + + def configure_channel(channel): + self.mmc.setConfig("channel", channel) + return channel + + print(configure_cam("Binning", -1)) + print(configure_cam("PixelReadoutRate", 0)) + print(configure_cam("Sensitivity/DynamicRange", 0)) + print(configure_channel("DAPI")) + def read(self) -> OrderedDict: data = OrderedDict() data['camera'] = {'value': self.image, 'timestamp': self.image_time} @@ -254,10 +297,10 @@ def read(self) -> OrderedDict: def describe(self): data = OrderedDict() - data['camera'] = {'source': self.mmc_device_name, - 'dtype': 'array', - 'shape' : self.image.shape} - return data + data['camera'] = {'source': self.mmc_device_name, + 'dtype': 'array', + 'shape': self.image.shape} + return data def subscribe(self, func): if not func in self._subscribers: @@ -268,3 +311,134 @@ def describe_configuration(self) -> OrderedDict: def read_configuration(self) -> OrderedDict: return OrderedDict() + + +class XYStage: + name = "xy" + parent = None + + def __init__(self, mmc): + self.mmc = mmc + self.mmc_device_name = self.mmc.getXYStageDevice() + + def trigger(self): + status = Status(obj=self, timeout=10) + + def wait(): + try: + self.mmc.waitForDevice(self.mmc_device_name) + except Exception as exc: + status.set_exception(exc) + else: + status.set_finished() + + threading.Thread(target=wait).start() + return status + + def read(self): + data = OrderedDict() + data['xy'] = {'value': self.mmc.getXYPosition(), + 'timestamp': time.time()} + return data + + def describe(self): + data = OrderedDict() + data['xy'] = {'source': "MMCore", + 'dtype': "number", + 'shape': []} + return data + + def set(self, value): + status = Status(obj=self, timeout=5) + + def wait(): + try: + self.mmc.setXYPosition(*value) + self.mmc.waitForDevice(self.mmc_device_name) + except Exception as exc: + status.set_exception(exc) + else: + status.set_finished() + + threading.Thread(target=wait).start() + + return status + + def read_configuration(self) -> OrderedDict: + return OrderedDict() + + def describe_configuration(self) -> OrderedDict: + return OrderedDict() + + +class SoftMMCPositioner(SignalPositionerMixin, Signal): + + _move_thread = None + + def __init__(self, *args, mmc=None, **kwargs): + self.mmc = get_mmc() + self.mmc_device_name = self.mmc.getXYStageDevice() + + super().__init__(*args, set_func=self._write_xy, **kwargs) + + # get the position from the controller on startup + self._readback = np.array(self.mmc.getXYPosition()) + + def _write_xy(self, value, **kwargs): + if self._move_thread is not None: + # The MoveStatus object defends us; this is just an additional safeguard. + # Do not ever expect to see this warning. + warnings.warn("Already moving. Will not start new move.") + st = MoveStatus(self, target=value) + + def moveXY(): + self.mmc.setXYPosition(*value) + # ALWAYS wait for the device + self.mmc.waitForDevice(self.mmc_device_name) + + # update the _readback attribute (which triggers other ophyd actions) + # np.array on the netref object forces conversion to np.array + self._readback = np.array(self.mmc.getXYPosition()) + + # MUST set to None BEFORE declaring status True + self._move_thread = None + st.set_finished() + + self._move_thread = threading.Thread(target=moveXY) + self._move_thread.start() + return st + + +class TwoD_XY_StagePositioner(PseudoPositioner): + + # The pseudo positioner axes: + x = Component(PseudoSingle, target_initial_position=True) + y = Component(PseudoSingle, target_initial_position=True) + + # The real (or physical) positioners: + # NOTE: ``mmc`` object MUST be defined`` first. + pair = Component(SoftMMCPositioner, mmc=get_mmc()) + + @pseudo_position_argument + def forward(self, pseudo_pos): + """Run a forward (pseudo -> real) calculation (return pair).""" + return self.RealPosition(pseudo_pos) + + # @real_position_argument + def inverse(self, real_pos): + """Run an inverse (real -> pseudo) calculation (return x & y).""" + if len(real_pos) == 1: + if real_pos.pair is None: + # as called from .move() + x, y = self.pair.mmc.getXYPosition() + else: + # initial call, get position from the hardware + x, y = tuple(real_pos.pair) + elif len(real_pos) == 2: + # as called directly + x, y = real_pos + else: + raise ValueError( + f"Incorrect argument: {self.name}.inverse({real_pos})" + ) + return self.PseudoPosition(x=x, y=y) diff --git a/deepthought/microscope.py b/deepthought/microscope.py index 3894d06..4261ea3 100644 --- a/deepthought/microscope.py +++ b/deepthought/microscope.py @@ -1,89 +1,73 @@ """ -microscope abstraction layer --- -handles all the abstractions of human API with the microscope. - - -construction --- -a microscope is made up of modular device components that work together -to orchestrate an experimental task. - -these modules are - 1. XYStage - 1. xy position - 2. limits of stage - 2. NosePiece - 1. Objectives - 2. Z value - 3. EnteringLight - 1. LightSources - 1. LED - 1. Intensity - 2. Wavelength - 2. Hallide - 1. Intensity - 2. Shutters - 3. Optics - 1. Mirrors - 2. Condensor - 4. ExitingLight - 1. ViewPorts - 1. Detectors - 1. exposure - 2. gain - 3. binning - 2. EyePiece - - -usage primitives --- -The microscope is fundamentally used by a user to make visual/abstract observations of the - samples thru one or more ViewPorts. - -The user configures the devices appropriately according to their wishes of -how the light should enter or exit the sample. - -For example: - if I want to image DAPI image, EnteringLight is configured such that - 1. LED is on, set to 405, with an intensity determined by a feedback - system. - -This abstraction allows us to group devices according to their functionality, -and create a more of an end-image centric organization of the device -primitives to define common microscopy tasks, which is what this section aims to encode. - -user API --- - -What a user does with their microscope, is their own business, but there are patterns -in usage that can be utilized to form an abstraction that can be used to generalize use -cases so as to code it into a system. Such abstractions are ideal design parameters for -APIs. - -We intend to map the user API with the System, in order to figure out the microscope -abstraction - - - -Notes --- -1. One can in-principle keep exposure constant and vary intensity of light source -2. 50-50 can be a ViewPort of the Detector kind, where there is an - exposure_factor of 2. +To do: + +1. configure number of cameras and camera parameters easily from user code. +2. xy, z into a position object. + Stage.x """ +import numpy as np +from bluesky import RunEngine +from bluesky.callbacks.best_effort import BestEffortCallback +from bluesky.plans import count, scan, spiral_square +from databroker import Broker +from configs import store_disk +from devices import Camera, Focus, TwoD_XY_StagePositioner, get_mmc -from devices import Camera, Focus # other devices have to be added. -# to figure out where +# to figure out where + + +bec = BestEffortCallback() +bec.disable_plots() + +db = Broker.named("temp") + + +RE = RunEngine({}) +RE.subscribe(bec) +RE.subscribe(db.insert) + +if store_disk: + from bluesky.callbacks.broker import LiveTiffExporter + + template = "output_dir/{start[scan_id]}/{event[seq_num]}.tiff" + live = LiveTiffExporter("camera", + template=template, + db=db, + overwrite=True) + RE.subscribe(live) class Microscope: def __init__(self): self.name = None + self.mmc = get_mmc() + self._cam = [Camera(self.mmc)] + self.z = Focus(self.mmc) + self.stage = TwoD_XY_StagePositioner("", name="xy_stage") + + def snap(self, num=1, delay=0): + # run a blue sky count method with cameras + # return uid + uid, = RE(count(self._cam, num=num, delay=delay)) + + # https://nsls-ii.github.io/databroker/generated/databroker.Header.table.html#databroker.Header.table + header = db[uid] + + img = np.stack(header.table()["camera"].array) + + return img + + def scan(self, center=None, range=None, num=None): + if center is None: + center = self.mmc.getXYPosition() + x_center, y_center = center - def snap(self): - # run a blue sky count method with detector - pass + plan = spiral_square(self._cam, self.stage.x, self.stage.y, x_center=x_center, y_center=y_center, + x_range=1000, y_range=1000, x_num=5, y_num=5) + uid, = RE(plan) + header = db[uid] + img = np.stack(header.table()["camera"].array) + return img diff --git a/deepthought/run.py b/deepthought/run.py index 2c3dafa..796d322 100644 --- a/deepthought/run.py +++ b/deepthought/run.py @@ -1,44 +1,14 @@ -from bluesky import RunEngine -from bluesky.callbacks.best_effort import BestEffortCallback -from bluesky.plans import count, scan -from databroker import Broker -from magicgui import magicgui - -from comms import client -from configs import config +from microscope import Microscope from detection import detect_object -from devices import Camera, Focus, SimMMC from viz import imshow -bec = BestEffortCallback() -bec.disable_plots() - -db = Broker.named("temp") - -# mmc = client(addr="10.10.1.62", port=18861).mmc -mmc = SimMMC() - -cam = Camera(mmc) -motor = Focus(mmc) - -RE = RunEngine({}) -RE.subscribe(bec) -RE.subscribe(db.insert) - -# decorate your function with the @magicgui decorator -@magicgui(call_button="snap", result_widget=True) -def snap(): - RE(count([cam], num=1)) - # to access the data, get the header object (of databroker) - # and access the data of camera - header = db[-1] +# bs - bright star microscope +bs = Microscope() +bs.mmc.setCameraDevice("right_port") - data = header.data("camera") - img = next(data) - (_, label) = detect_object(img, kind="dapi") - stage_coords = mmc.getXYPosition() +imgs = bs.snap() - imshow(img, label, stage_coords) -snap.show(run=True) +labels = detect_object(imgs) +imshow(imgs, labels) diff --git a/deepthought/viz.py b/deepthought/viz.py index a48ba96..2067695 100644 --- a/deepthought/viz.py +++ b/deepthought/viz.py @@ -46,8 +46,16 @@ def transform_xy(x, y, stage_coords): return list(zip(x, y)) +def imshow(image, label_image=None, *args, **kwargs): + with napari.gui_qt(): + viewer = napari.view_image(image, name="image") + + if label_image is not None: + print("here") + viewer.add_labels(label_image, visible=False, name="segments") + -def imshow(image, label_image, stage_coords): +def imshow_sp(image, label_image, stage_coords): with napari.gui_qt(): viewer = napari.view_image(image, name="DAPI")