diff --git a/pom.xml b/pom.xml index 7b4faaa..2f4950f 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.micro-manager.acqengj AcqEngJ - 0.26.6 + 0.27.0 jar AcqEngJ Java-based Acquisition engine for Micro-Manager diff --git a/src/main/java/org/micromanager/acqj/api/AcquisitionAPI.java b/src/main/java/org/micromanager/acqj/api/AcquisitionAPI.java index 0d380c3..655ff66 100644 --- a/src/main/java/org/micromanager/acqj/api/AcquisitionAPI.java +++ b/src/main/java/org/micromanager/acqj/api/AcquisitionAPI.java @@ -19,10 +19,23 @@ */ public interface AcquisitionAPI { - public static final int EVENT_GENERATION_HOOK = 0; - public static final int BEFORE_HARDWARE_HOOK = 1; - public static final int AFTER_HARDWARE_HOOK = 2; - public static final int AFTER_CAMERA_HOOK = 3; + int EVENT_GENERATION_HOOK = 0; + + // This hook runs before changes to the hardware (corresponding to the instructions in the + // event) are made + int BEFORE_HARDWARE_HOOK = 1; + + // This hook runs after changes to the hardware took place, but before camera exposure + // (either a snap or a sequence) is started + int AFTER_HARDWARE_HOOK = 2; + + // Hook runs after the camera sequence acquisition has started. This can be used for + // external triggering of the camera + int AFTER_CAMERA_HOOK = 3; + + // Hook runs after the camera exposure ended (when possible, before readout of the camera + // and availability of the images in memory). + int AFTER_EXPOSURE_HOOK = 4; /** * Call to ready acquisition to start receiving acquisiton events. No more hooks diff --git a/src/main/java/org/micromanager/acqj/internal/Engine.java b/src/main/java/org/micromanager/acqj/internal/Engine.java index 7f9fd4f..3066f8f 100644 --- a/src/main/java/org/micromanager/acqj/internal/Engine.java +++ b/src/main/java/org/micromanager/acqj/internal/Engine.java @@ -16,10 +16,9 @@ // package org.micromanager.acqj.internal; -/* - * To change this template, choose Tools | Templates and open the template in - * the editor. - */ +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import org.micromanager.acqj.api.AcquisitionAPI; import org.micromanager.acqj.main.AcquisitionEvent; import org.micromanager.acqj.api.AcquisitionHook; @@ -55,6 +54,7 @@ public class Engine { private static ExecutorService eventGeneratorExecutor_; //Thread on which all communication with hardware occurs private static ExecutorService acqExecutor_; + private static ExecutorService afterExposureExecutor_; public Engine(CMMCore core) { @@ -64,6 +64,8 @@ public Engine(CMMCore core) { acqExecutor_ = Executors.newSingleThreadExecutor(r -> { return new Thread(r, "Acquisition Engine Thread"); }); + afterExposureExecutor_ = Executors.newSingleThreadExecutor(r -> + new Thread(r, "After Exposure Thread")); eventGeneratorExecutor_ = Executors .newSingleThreadExecutor((Runnable r) -> new Thread(r, "Acq Eng event generator")); } @@ -290,6 +292,10 @@ private void executeAcquisitionEvent(AcquisitionEvent event) throws InterruptedE h.run(event); h.close(); } + for (AcquisitionHook h : event.acquisition_.getAfterExposureHooks()) { + h.run(event); + h.close(); + } event.acquisition_.addToOutput(new TaggedImage(null, null)); event.acquisition_.markEventsFinished(); } else { @@ -348,39 +354,53 @@ private void executeAcquisitionEvent(AcquisitionEvent event) throws InterruptedE return; } - private void acquireImages(AcquisitionEvent event) throws HardwareControlException { - acquireImages(event, true); - } - /** * Acquire 1 or more images in a sequence, add some metadata, then - * put them into an output queue + * put them into an output queue. * * @param event - * @param runAfterCameraHook * @throws HardwareControlException */ - private void acquireImages(final AcquisitionEvent event, - boolean runAfterCameraHook) throws HardwareControlException { + private void acquireImages(final AcquisitionEvent event) throws HardwareControlException { + FutureTask future = null; try { if (event.getSequence() != null && event.getSequence().size() > 1) { //start hardware sequence // core_.clearCircularBuffer(); - if (event.getSequence().stream().filter(e -> e.getCameraDeviceName() != null).count() == 0) { - // Using the default (Core-Camera) - core_.startSequenceAcquisition(event.getSequence().size(), 0, true); - } else { - // figure out how many images on each camera and start sequence with appropriate number on each - HashMap cameraImageCounts = new HashMap(); - Set cameraDeviceNames = new HashSet(); - event.getSequence().stream().map(e -> e.getCameraDeviceName()).forEach(e -> cameraDeviceNames.add(e)); - for (String cameraDeviceName : cameraDeviceNames) { - cameraImageCounts.put(cameraDeviceName, (int) event.getSequence().stream().filter( - e -> e.getCameraDeviceName() != null && e.getCameraDeviceName().equals(cameraDeviceName)).count()); - core_.startSequenceAcquisition(cameraDeviceName, cameraImageCounts.get(cameraDeviceName), 0, true); - } + // figure out how many images on each camera and start sequence with appropriate number on each + HashMap cameraImageCounts = new HashMap(); + Set cameraDeviceNames = new HashSet(); + event.getSequence().stream().map(e -> e.getCameraDeviceName()).forEach(e -> cameraDeviceNames.add(e)); + cameraDeviceNames.remove(null); + if (cameraDeviceNames.isEmpty()) { + cameraDeviceNames.add(core_.getCameraDevice()); } + for (String cameraDeviceName : cameraDeviceNames) { + cameraImageCounts.put(cameraDeviceName, (int) event.getSequence().stream().filter( + e -> e.getCameraDeviceName() != null && + e.getCameraDeviceName().equals(cameraDeviceName)).count()); + if (cameraDeviceNames.size() == 1 && cameraDeviceName.equals(core_.getCameraDevice())) { + cameraImageCounts.put(cameraDeviceName, event.getSequence().size()); + } + core_.startSequenceAcquisition(cameraDeviceName, + cameraImageCounts.get(cameraDeviceName), 0, true); + } + // Run after exposure hooks on a separate thread that checks if + // sequence finished. AcquireImages can only exit after the last + // future returns. + future = new FutureTask<>(() -> { + for (String cameraDeviceName : cameraDeviceNames) { + while (core_.isSequenceRunning(cameraDeviceName)) { + Thread.sleep(1); + } + } + for (AcquisitionHook h : event.acquisition_.getAfterExposureHooks()) { + h.run(event); + } + return null; + }); + afterExposureExecutor_.execute(future); } else { //snap one image with no sequencing @@ -391,6 +411,13 @@ private void acquireImages(final AcquisitionEvent event, core_.setCameraDevice(currentCamera); } else { core_.snapImage(); + // note: SnapImage will block until exposure finishes. + // If it is desired that AfterCameraHooks trigger cameras + // in Snap mode, those hooks (or SnapImage) should run in a separate thread, started + // after snapImage is started. + for (AcquisitionHook h : event.acquisition_.getAfterExposureHooks()) { + h.run(event); + } } } } catch (Exception ex) { @@ -417,18 +444,16 @@ private void acquireImages(final AcquisitionEvent event, } } - //Run a hook after the camera sequence acquistion has started. This is used for setups - //where an external device is used to trigger the camera - if (runAfterCameraHook) { - for (AcquisitionHook h : event.acquisition_.getAfterCameraHooks()) { - h.run(event); - } + // Run a hook after the camera sequence acquisition has started. This can be used for + // external triggering of the camera (when it is in sequence mode). + for (AcquisitionHook h : event.acquisition_.getAfterCameraHooks()) { + h.run(event); } if (event.acquisition_.isDebugMode()) { core_.logMessage("images acquired, adding to output" ); } - //Loop through and collect all acquired images. There will be + // Loop through and collect all acquired images. There will be // (# of images in sequence) x (# of camera channels) of them for (int i = 0; i < (event.getSequence() == null ? 1 : event.getSequence().size()); i++) { double exposure; @@ -512,6 +537,18 @@ private void acquireImages(final AcquisitionEvent event, correspondingEvent.acquisition_.addToImageMetadata(ti.tags); correspondingEvent.acquisition_.addToOutput(ti); + + } + } + if (future != null) { + try { + future.get(); + } catch (InterruptedException e) { + e.printStackTrace(); + core_.logMessage("Interrupted while waiting for the future"); + } catch (ExecutionException e) { + e.printStackTrace(); + core_.logMessage("Exception during future execution"); } } } @@ -883,7 +920,7 @@ public void run() { } } - }, "Changing exposure"); + }, "Changing additional properties"); //keep track of last event to know what state the hardware was in without having to query it lastEvent_ = event.getSequence() == null ? event : event.getSequence().get(event.getSequence().size() - 1); diff --git a/src/main/java/org/micromanager/acqj/main/Acquisition.java b/src/main/java/org/micromanager/acqj/main/Acquisition.java index ae1fac0..0fbcfc9 100644 --- a/src/main/java/org/micromanager/acqj/main/Acquisition.java +++ b/src/main/java/org/micromanager/acqj/main/Acquisition.java @@ -51,6 +51,7 @@ public class Acquisition implements AcquisitionAPI { private CopyOnWriteArrayList beforeHardwareHooks_ = new CopyOnWriteArrayList(); private CopyOnWriteArrayList afterHardwareHooks_ = new CopyOnWriteArrayList(); private CopyOnWriteArrayList afterCameraHooks_ = new CopyOnWriteArrayList(); + private CopyOnWriteArrayList afterExposureHooks_ = new CopyOnWriteArrayList<>(); private CopyOnWriteArrayList imageProcessors_ = new CopyOnWriteArrayList(); protected LinkedBlockingDeque firstDequeue_ = new LinkedBlockingDeque(IMAGE_QUEUE_SIZE); @@ -240,7 +241,7 @@ public void addImageProcessor(TaggedImageProcessor p) { @Override public void addHook(AcquisitionHook h, int type) { if (started_) { - throw new RuntimeException("Cannot add hook after acquisiton started"); + throw new RuntimeException("Cannot add hook after acquisition started"); } if (type == EVENT_GENERATION_HOOK) { eventGenerationHooks_.add(h); @@ -250,12 +251,14 @@ public void addHook(AcquisitionHook h, int type) { afterHardwareHooks_.add(h); } else if (type == AFTER_CAMERA_HOOK) { afterCameraHooks_.add(h); + } else if (type == AFTER_EXPOSURE_HOOK) { + afterExposureHooks_.add(h); } } @Override /** - * Block until everything finished + * Block until everything finished. */ public void waitForCompletion() { try { @@ -363,9 +366,17 @@ public Iterable getBeforeHardwareHooks() { return beforeHardwareHooks_; } - public Iterable getAfterHardwareHooks() { return afterHardwareHooks_; } + public Iterable getAfterHardwareHooks() { + return afterHardwareHooks_; + } + + public Iterable getAfterCameraHooks() { + return afterCameraHooks_; + } - public Iterable getAfterCameraHooks() { return afterCameraHooks_; } + public Iterable getAfterExposureHooks() { + return afterExposureHooks_; + } public void addToOutput(TaggedImage ti) { try { diff --git a/src/main/java/org/micromanager/acqj/main/AcquisitionEvent.java b/src/main/java/org/micromanager/acqj/main/AcquisitionEvent.java index 7b89b24..9b59336 100644 --- a/src/main/java/org/micromanager/acqj/main/AcquisitionEvent.java +++ b/src/main/java/org/micromanager/acqj/main/AcquisitionEvent.java @@ -32,7 +32,7 @@ import org.micromanager.acqj.internal.Engine; /** - * Information about the acquisition of a single image or a sequence of image + * Information about the acquisition of a single image or a sequence of image. * */ public class AcquisitionEvent { @@ -71,7 +71,7 @@ enum SpecialFlag { //Pattern to project onto SLM. Can either be int[] or byte[] private Object slmImage_ = null; - //Arbitary additional properties + //Arbitrary additional properties private TreeSet properties_ = new TreeSet(); //for hardware sequencing @@ -86,7 +86,7 @@ public AcquisitionEvent(AcquisitionAPI acq) { } /** - * Constructor used for running a list of events in a sequence It should have + * Constructor used for running a list of events in a sequence. It should have * already been verified that these events are sequencable. This constructor * figures out which device types need a sequence and which ones can be left * with a single value