class Acquisition(object): """ """ def __init__( self, directory=None, name=None, image_process_fn=None, event_generation_hook_fn=None, pre_hardware_hook_fn=None, post_hardware_hook_fn=None, post_camera_hook_fn=None, show_display=True, tile_overlap=None, max_multi_res_index=None, magellan_acq_index=None, magellan_explore=False, process=False, debug=False, ): """ Parameters ---------- directory : str saving directory for this acquisition. Required unless an image process function will be implemented that diverts images from saving name : str Saving name for the acquisition. Required unless an image process function will be implemented that diverts images from saving image_process_fn : Callable image processing function that will be called on each image that gets acquired. Can either take two arguments (image, metadata) where image is a numpy array and metadata is a dict containing the corresponding iamge metadata. Or a 4 argument version is accepted, which accepts (image, metadata, bridge, queue), where bridge and queue are an instance of the pycromanager.acquire.Bridge object for the purposes of interacting with arbitrary code on the Java side (such as the micro-manager core), and queue is a Queue objects that holds upcomning acquisition events. Both version must either return event_generation_hook_fn : Callable hook function that will as soon as acquisition events are generated (before hardware sequencing optimization in the acquisition engine. This is useful if one wants to modify acquisition events that they didn't generate (e.g. those generated by a GUI application). Accepts either one argument (the current acquisition event) or three arguments (current event, bridge, event Queue) pre_hardware_hook_fn : Callable hook function that will be run just before the hardware is updated before acquiring a new image. In the case of hardware sequencing, it will be run just before a sequence of instructions are dispatched to the hardware. Accepts either one argument (the current acquisition event) or three arguments (current event, bridge, event Queue) post_hardware_hook_fn : Callable hook function that will be run just before the hardware is updated before acquiring a new image. In the case of hardware sequencing, it will be run just after a sequence of instructions are dispatched to the hardware, but before the camera sequence has been started. Accepts either one argument (the current acquisition event) or three arguments (current event, bridge, event Queue) post_camera_hook_fn : Callable hook function that will be run just after the camera has been triggered to snapImage or startSequence. A common use case for this hook is when one want to send TTL triggers to the camera from an external timing device that synchronizes with other hardware. Accepts either one argument (the current acquisition event) or three arguments (current event, bridge, event Queue) tile_overlap : int or tuple of int If given, XY tiles will be laid out in a grid and multi-resolution saving will be actived. Argument can be a two element tuple describing the pixel overlaps between adjacent tiles. i.e. (pixel_overlap_x, pixel_overlap_y), or an integer to use the same overlap for both. For these features to work, the current hardware configuration must have a valid affine transform between camera coordinates and XY stage coordinates max_multi_res_index : int Maximum index to downsample to in multi-res pyramid mode (which is only active if a value for "tile_overlap" is passed in, or if running a Micro-Magellan acquisition). 0 is no downsampling, 1 is downsampled up to 2x, 2 is downsampled up to 4x, etc. If not provided, it will be dynamically calculated and updated from data show_display : bool show the image viewer window magellan_acq_index : int run this acquisition using the settings specified at this position in the main GUI of micro-magellan (micro-manager plugin). This index starts at 0 magellan_explore : bool Run a Micro-magellan explore acquisition process : bool Use multiprocessing instead of multithreading for acquisition hooks and image processors. This can be used to speed up CPU-bounded processing by eliminating bottlenecks caused by Python's Global Interpreter Lock, but also creates complications on Windows-based systems debug : bool whether to print debug messages """ self.bridge = Bridge(debug=debug) self._debug = debug self._dataset = None if directory is not None: # Expend ~ in path directory = os.path.expanduser(directory) # If path is relative, retain knowledge of the current working directory directory = os.path.abspath(directory) if magellan_acq_index is not None: magellan_api = self.bridge.get_magellan() self._remote_acq = magellan_api.create_acquisition( magellan_acq_index) self._event_queue = None elif magellan_explore: magellan_api = self.bridge.get_magellan() self._remote_acq = magellan_api.create_explore_acquisition() self._event_queue = None else: # Create thread safe queue for events so they can be passed from multiple processes self._event_queue = multiprocessing.Queue( ) if process else queue.Queue() core = self.bridge.get_core() acq_factory = self.bridge.construct_java_object( "org.micromanager.remote.RemoteAcquisitionFactory", args=[core]) show_viewer = show_display and (directory is not None and name is not None) if tile_overlap is None: # argument placeholders, these wont actually be used x_overlap = 0 y_overlap = 0 else: if type(tile_overlap) is tuple: x_overlap, y_overlap = tile_overlap else: x_overlap = tile_overlap y_overlap = tile_overlap self._remote_acq = acq_factory.create_acquisition( directory, name, show_viewer, tile_overlap is not None, x_overlap, y_overlap, max_multi_res_index if max_multi_res_index is not None else -1, ) storage = self._remote_acq.get_data_sink() if storage is not None: self.disk_location = storage.get_disk_location() if image_process_fn is not None: processor = self.bridge.construct_java_object( "org.micromanager.remote.RemoteImageProcessor") self._remote_acq.add_image_processor(processor) self._start_processor(processor, image_process_fn, self._event_queue, process=process) if event_generation_hook_fn is not None: hook = self.bridge.construct_java_object( "org.micromanager.remote.RemoteAcqHook", args=[self._remote_acq]) self._start_hook(hook, event_generation_hook_fn, self._event_queue, process=process) self._remote_acq.add_hook(hook, self._remote_acq.EVENT_GENERATION_HOOK) if pre_hardware_hook_fn is not None: hook = self.bridge.construct_java_object( "org.micromanager.remote.RemoteAcqHook", args=[self._remote_acq]) self._start_hook(hook, pre_hardware_hook_fn, self._event_queue, process=process) self._remote_acq.add_hook(hook, self._remote_acq.BEFORE_HARDWARE_HOOK) if post_hardware_hook_fn is not None: hook = self.bridge.construct_java_object( "org.micromanager.remote.RemoteAcqHook", args=[self._remote_acq]) self._start_hook(hook, post_hardware_hook_fn, self._event_queue, process=process) self._remote_acq.add_hook(hook, self._remote_acq.AFTER_HARDWARE_HOOK) if post_camera_hook_fn is not None: hook = self.bridge.construct_java_object( "org.micromanager.remote.RemoteAcqHook", args=[self._remote_acq]) self._start_hook(hook, post_camera_hook_fn, self._event_queue, process=process) self._remote_acq.add_hook(hook, self._remote_acq.AFTER_CAMERA_HOOK) self._remote_acq.start() if magellan_acq_index is None and not magellan_explore: self.event_port = self._remote_acq.get_event_port() self.event_process = threading.Thread( target=_event_sending_fn, args=(self.event_port, self._event_queue, self._debug), name="Event sending", ) self.event_process.start() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): if self._event_queue is not None: # magellan acquisitions dont have this # this should shut down storage and viewer as apporpriate self._event_queue.put(None) # now wait on it to finish self.await_completion() def get_disk_location(self): """ Return the path where the dataset is on disk """ return self._remote_acq.get_storage().get_disk_location() def get_dataset(self): """ """ if self._dataset is None: self._dataset = Dataset( remote_storage=self._remote_acq.get_storage()) return self._dataset def await_completion(self): """Wait for acquisition to finish and resources to be cleaned up""" while not self._remote_acq.is_finished(): time.sleep(0.1) def acquire(self, events, keep_shutter_open=False): """Submit an event or a list of events for acquisition. Optimizations (i.e. taking advantage of hardware synchronization, where available), will take place across this list of events, but not over multiple calls of this method. A single event is a python dictionary with a specific structure Parameters ---------- events keep_shutter_open : (Default value = False) Returns ------- """ if keep_shutter_open and isinstance(events, list): for e in events: e["keep_shutter_open"] = True events.append({"keep_shutter_open": False }) # return to autoshutter, dont acquire an image elif keep_shutter_open and isinstance(events, dict): events["keep_shutter_open"] = True events = [ events, { "keep_shutter_open": False }, ] # return to autoshutter, dont acquire an image self._event_queue.put(events) def _start_hook(self, remote_hook, remote_hook_fn, event_queue, process): """ Parameters ---------- remote_hook : remote_hook_fn : event_queue : process : Returns ------- """ hook_connected_evt = multiprocessing.Event( ) if process else threading.Event() pull_port = remote_hook.get_pull_port() push_port = remote_hook.get_push_port() hook_thread = (multiprocessing.Process if process else threading.Thread)( target=_acq_hook_startup_fn, name="AcquisitionHook", args=( pull_port, push_port, hook_connected_evt, event_queue, remote_hook_fn, self._debug, ), ) # if process else threading.Thread(target=_acq_hook_fn, args=(), name='AcquisitionHook') hook_thread.start() hook_connected_evt.wait() # wait for push/pull sockets to connect def _start_processor(self, processor, process_fn, event_queue, process): """ Parameters ---------- processor : process_fn : event_queue : process : Returns ------- """ # this must start first processor.start_pull() sockets_connected_evt = multiprocessing.Event( ) if process else threading.Event() pull_port = processor.get_pull_port() push_port = processor.get_push_port() self.processor_thread = (multiprocessing.Process if process else threading.Thread)( target=_processor_startup_fn, args=( pull_port, push_port, sockets_connected_evt, process_fn, event_queue, self._debug, ), name="ImageProcessor", ) self.processor_thread.start() sockets_connected_evt.wait() # wait for push/pull sockets to connect processor.start_push()
class Acquisition(object): def __init__(self, directory=None, name=None, image_process_fn=None, pre_hardware_hook_fn=None, post_hardware_hook_fn=None, tile_overlap=None, magellan_acq_index=None, process=True, debug=False): """ :param directory: saving directory for this acquisition. Required unless an image process function will be implemented that diverts images from saving :type directory: str :param name: Saving name for the acquisition. Required unless an image process function will be implemented that diverts images from saving :type name: str :param image_process_fn: image processing function that will be called on each image that gets acquired. Can either take two arguments (image, metadata) where image is a numpy array and metadata is a dict containing the corresponding iamge metadata. Or a 4 argument version is accepted, which accepts (image, metadata, bridge, queue), where bridge and queue are an instance of the pycromanager.acquire.Bridge object for the purposes of interacting with arbitrary code on the Java side (such as the micro-manager core), and queue is a Queue objects that holds upcomning acquisition events. Both version must either return :param pre_hardware_hook_fn: hook function that will be run just before the hardware is updated before acquiring a new image. Accepts either one argument (the current acquisition event) or three arguments (current event, bridge, event Queue) :param post_hardware_hook_fn: hook function that will be run just before the hardware is updated before acquiring a new image. Accepts either one argument (the current acquisition event) or three arguments (current event, bridge, event Queue) :param tile_overlap: If given, XY tiles will be laid out in a grid and multi-resolution saving will be actived. Argument can be a two element tuple describing the pixel overlaps between adjacent tiles. i.e. (pixel_overlap_x, pixel_overlap_y), or an integer to use the same overlap for both. For these features to work, the current hardware configuration must have a valid affine transform between camera coordinates and XY stage coordinates :type tile_overlap: tuple, int :param magellan_acq_index: run this acquisition using the settings specified at this position in the main GUI of micro-magellan (micro-manager plugin). This index starts at 0 :type magellan_acq_index: int :param process: (Experimental) use multiprocessing instead of multithreading for acquisition hooks and image processors :type process: boolean :param debug: print debugging stuff :type debug: boolean """ self.bridge = Bridge(debug=debug) self._debug = debug self._dataset = None if directory is not None: # Expend ~ in path directory = os.path.expanduser(directory) # If path is relative, retain knowledge of the current working directory directory = os.path.abspath(directory) if magellan_acq_index is not None: magellan_api = self.bridge.get_magellan() self._remote_acq = magellan_api.create_acquisition( magellan_acq_index) self._event_queue = None else: # Create thread safe queue for events so they can be passed from multiple processes self._event_queue = multiprocessing.Queue() core = self.bridge.get_core() acq_factory = self.bridge.construct_java_object( 'org.micromanager.remote.RemoteAcquisitionFactory', args=[core]) #TODO: could add hiding viewer as an option show_viewer = directory is not None and name is not None if tile_overlap is None: #argument placeholders, these wont actually be used x_overlap = 0 y_overlap = 0 else: if type(tile_overlap) is tuple: x_overlap, y_overlap = tile_overlap else: x_overlap = tile_overlap y_overlap = tile_overlap self._remote_acq = acq_factory.create_acquisition( directory, name, show_viewer, tile_overlap is not None, x_overlap, y_overlap) if image_process_fn is not None: processor = self.bridge.construct_java_object( 'org.micromanager.remote.RemoteImageProcessor') self._remote_acq.add_image_processor(processor) self._start_processor(processor, image_process_fn, self._event_queue, process=process) if pre_hardware_hook_fn is not None: hook = self.bridge.construct_java_object( 'org.micromanager.remote.RemoteAcqHook') self._start_hook(hook, pre_hardware_hook_fn, self._event_queue, process=process) self._remote_acq.add_hook(hook, self._remote_acq.BEFORE_HARDWARE_HOOK, args=[self._remote_acq]) if post_hardware_hook_fn is not None: hook = self.bridge.construct_java_object( 'org.micromanager.remote.RemoteAcqHook', args=[self._remote_acq]) self._start_hook(hook, post_hardware_hook_fn, self._event_queue, process=process) self._remote_acq.add_hook(hook, self._remote_acq.AFTER_HARDWARE_HOOK) self._remote_acq.start() if magellan_acq_index is None: self.event_port = self._remote_acq.get_event_port() self.event_process = multiprocessing.Process( target=_event_sending_fn, args=(self.event_port, self._event_queue, self._debug), name='Event sending') # if multiprocessing else threading.Thread(target=event_sending_fn, args=(), name='Event sending') self.event_process.start() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): if self._event_queue is not None: #magellan acquisitions dont have this # this should shut down storage and viewer as apporpriate self._event_queue.put(None) #now wait on it to finish self.await_completion() def get_dataset(self): """ Return a :class:`~pycromanager.data.Dataset` object that has access to the underlying pixels :return: :class:`~pycromanager.data.Dataset` corresponding to this acquisition """ if self._dataset is None: self._dataset = Dataset( remote_storage=self._remote_acq.get_storage()) return self._dataset def await_completion(self): """ Wait for acquisition to finish and resources to be cleaned up """ while (not self._remote_acq.is_finished()): time.sleep(0.1) def acquire(self, events): """ Submit an event or a list of events for acquisition. Optimizations (i.e. taking advantage of hardware synchronization, where available), will take place across this list of events, but not over multiple calls of this method. A single event is a python dictionary with a specific structure :param events: single event (i.e. a dictionary) or a list of events """ self._event_queue.put(events) def _start_hook(self, remote_hook, remote_hook_fn, event_queue, process): hook_connected_evt = multiprocessing.Event( ) if process else threading.Event() pull_port = remote_hook.get_pull_port() push_port = remote_hook.get_push_port() hook_thread = multiprocessing.Process( target=_acq_hook_startup_fn, name='AcquisitionHook', args=(pull_port, push_port, hook_connected_evt, event_queue, remote_hook_fn, self._debug)) # if process else threading.Thread(target=_acq_hook_fn, args=(), name='AcquisitionHook') hook_thread.start() hook_connected_evt.wait() # wait for push/pull sockets to connect def _start_processor(self, processor, process_fn, event_queue, process): # this must start first processor.start_pull() sockets_connected_evt = multiprocessing.Event( ) if process else threading.Event() pull_port = processor.get_pull_port() push_port = processor.get_push_port() self.processor_thread = multiprocessing.Process( target=_processor_startup_fn, args=(pull_port, push_port, sockets_connected_evt, process_fn, event_queue, self._debug), name='ImageProcessor') # if multiprocessing else threading.Thread(target=other_thread_fn, args=(), name='ImageProcessor') self.processor_thread.start() sockets_connected_evt.wait() # wait for push/pull sockets to connect processor.start_push()
def start_headless(mm_app_path, config_file, java_loc=None, core_log_path=None, buffer_size_mb=1024): """ Start a Java process that contains the neccessary libraries for pycro-manager to run, so that it can be run independently of the Micro-Manager GUI/application. This call will create and initialize MMCore with the configuration file provided. On windows plaforms, the Java Runtime Environment will be grabbed automatically as it is installed along with the Micro-Manager application. On non-windows platforms, it may need to be installed/specified manually in order to ensure compatibility. This can be checked by looking at the "maven.compiler.source" entry which the java parts of pycro-manager were compiled with. See here: https://github.com/micro-manager/pycro-manager/blob/29b584bfd71f0d05750f5d39600318902186a06a/java/pom.xml#L8 Parameters ---------- mm_app_path : str Path to top level folder of Micro-Manager installation (made with graphical installer) config_file : str Path to micro-manager config file, with which core will be initialized java_loc: str Path to the java version that it should be run with core_log_path : str Path to where core log files should be created buffer_size_mb : int Size of circular buffer in MB in MMCore """ classpath = '"' + mm_app_path + '/plugins/Micro-Manager/*"' if java_loc is None: if platform.system() == "Windows": # windows comes with its own JRE java_loc = mm_app_path + "/jre/bin/javaw.exe" else: java_loc = "java" # This starts Java process and instantiates essential objects (core, # acquisition engine, ZMQServer) p = subprocess.Popen([ java_loc, "-classpath", classpath, "-Dsun.java2d.dpiaware=false", "-Xmx2000m", # This is used by MM desktop app but breaks things on MacOS...Don't think its neccessary # "-XX:MaxDirectMemorySize=1000", "org.micromanager.remote.HeadlessLauncher", ]) # make sure Java process cleans up when Python process exits atexit.register(lambda: p.terminate()) # Initialize core bridge = Bridge() core = bridge.get_core() core.wait_for_system() core.load_system_configuration(config_file) core.set_circular_buffer_memory_footprint(buffer_size_mb) if core_log_path is not None: core.enable_stderr_log(True) core.enable_debug_log(True) core.set_primary_log_file(core_log_path)