Java tutorial
/////////////////////////////////////////////////////////////////////////////// //FILE: AcquisitionPanel.java //PROJECT: Micro-Manager //SUBSYSTEM: ASIdiSPIM plugin //----------------------------------------------------------------------------- // // AUTHOR: Nico Stuurman, Jon Daniels // // COPYRIGHT: University of California, San Francisco, & ASI, 2013 // // LICENSE: This file is distributed under the BSD license. // License text is included with the source distribution. // // This file is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // // IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. package org.micromanager.asidispim; import org.micromanager.asidispim.Data.AcquisitionModes; import org.micromanager.asidispim.Data.CameraModes; import org.micromanager.asidispim.Data.Cameras; import org.micromanager.asidispim.Data.Devices; import org.micromanager.asidispim.Data.MultichannelModes; import org.micromanager.asidispim.Data.MyStrings; import org.micromanager.asidispim.Data.Positions; import org.micromanager.asidispim.Data.Prefs; import org.micromanager.asidispim.Data.Properties; import org.micromanager.asidispim.Data.AcquisitionSettings; import org.micromanager.asidispim.Data.ChannelSpec; import org.micromanager.asidispim.Data.Joystick.Directions; import org.micromanager.asidispim.Utils.DevicesListenerInterface; import org.micromanager.asidispim.Utils.ListeningJPanel; import org.micromanager.asidispim.Utils.MyDialogUtils; import org.micromanager.asidispim.Utils.MyNumberUtils; import org.micromanager.asidispim.Utils.PanelUtils; import org.micromanager.asidispim.Utils.SliceTiming; import org.micromanager.asidispim.Utils.StagePositionUpdater; import org.micromanager.asidispim.Utils.ControllerUtils; import org.micromanager.asidispim.Utils.AutofocusUtils; import org.micromanager.asidispim.Utils.MovementDetector; import org.micromanager.asidispim.Utils.MovementDetector.Method; import org.micromanager.asidispim.api.ASIdiSPIMException; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Insets; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.awt.geom.Point2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.PrintWriter; import java.text.ParseException; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JFormattedTextField; import javax.swing.JOptionPane; import javax.swing.JTextField; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JComboBox; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.JToggleButton; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.text.DefaultFormatter; import javax.swing.BorderFactory; import net.miginfocom.swing.MigLayout; import org.json.JSONException; import org.json.JSONObject; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.swtdesigner.SwingResourceManager; import mmcorej.CMMCore; import mmcorej.StrVector; import mmcorej.TaggedImage; import org.micromanager.api.MultiStagePosition; import org.micromanager.api.StagePosition; import org.micromanager.api.PositionList; import org.micromanager.api.ScriptInterface; import org.micromanager.api.ImageCache; import org.micromanager.api.MMTags; import org.micromanager.MMStudio; import org.micromanager.acquisition.ComponentTitledBorder; import org.micromanager.acquisition.DefaultTaggedImageSink; import org.micromanager.acquisition.MMAcquisition; import org.micromanager.acquisition.TaggedImageQueue; import org.micromanager.acquisition.TaggedImageStorageDiskDefault; import org.micromanager.acquisition.TaggedImageStorageMultipageTiff; import org.micromanager.imagedisplay.VirtualAcquisitionDisplay; import org.micromanager.utils.ImageUtils; import org.micromanager.utils.NumberUtils; import org.micromanager.utils.FileDialogs; import org.micromanager.utils.MDUtils; import org.micromanager.utils.MMFrame; import org.micromanager.utils.MMScriptException; import org.micromanager.utils.ReportingUtils; import ij.IJ; import org.apache.commons.math3.geometry.euclidean.threed.Rotation; import org.apache.commons.math3.geometry.euclidean.threed.Vector3D; /** * * @author nico * @author Jon */ @SuppressWarnings("serial") public class AcquisitionPanel extends ListeningJPanel implements DevicesListenerInterface { private final Devices devices_; private final Properties props_; private final Cameras cameras_; private final Prefs prefs_; private final ControllerUtils controller_; private final AutofocusUtils autofocus_; private final Positions positions_; private final CMMCore core_; private final ScriptInterface gui_; private final JCheckBox advancedSliceTimingCB_; private final JSpinner numSlices_; private final JComboBox numSides_; private final JComboBox firstSide_; private final JSpinner numScansPerSlice_; private final JSpinner lineScanDuration_; private final JSpinner delayScan_; private final JSpinner delayLaser_; private final JSpinner delayCamera_; private final JSpinner durationCamera_; // NB: not the same as camera exposure private final JSpinner exposureCamera_; // NB: only used in advanced timing mode private final JCheckBox alternateBeamScanCB_; private final JSpinner durationLaser_; private final JSpinner delaySide_; private final JLabel actualSlicePeriodLabel_; private final JLabel actualVolumeDurationLabel_; private final JLabel actualTimeLapseDurationLabel_; private final JSpinner numTimepoints_; private final JSpinner acquisitionInterval_; private final JToggleButton buttonStart_; private final JButton buttonTestAcq_; private final JPanel volPanel_; private final JPanel sliceAdvancedPanel_; private final JPanel timepointPanel_; private final JPanel savePanel_; private final JPanel durationPanel_; private final JFormattedTextField rootField_; private final JFormattedTextField prefixField_; private final JLabel acquisitionStatusLabel_; private int numTimePointsDone_; private final AtomicBoolean cancelAcquisition_ = new AtomicBoolean(false); // true if we should stop acquisition private final AtomicBoolean acquisitionRequested_ = new AtomicBoolean(false); // true if acquisition has been requested to start or is underway private final AtomicBoolean acquisitionRunning_ = new AtomicBoolean(false); // true if the acquisition is actually underway private final StagePositionUpdater posUpdater_; private final JSpinner stepSize_; private final JLabel desiredSlicePeriodLabel_; private final JSpinner desiredSlicePeriod_; private final JLabel desiredLightExposureLabel_; private final JSpinner desiredLightExposure_; private final JCheckBox minSlicePeriodCB_; private final JCheckBox separateTimePointsCB_; private final JCheckBox saveCB_; private final JComboBox spimMode_; private final JCheckBox navigationJoysticksCB_; private final JCheckBox usePositionsCB_; private final JSpinner positionDelay_; private final JCheckBox useTimepointsCB_; private final JCheckBox useAutofocusCB_; private final JCheckBox useMovementCorrectionCB_; private final JPanel leftColumnPanel_; private final JPanel centerColumnPanel_; private final JPanel rightColumnPanel_; private final MMFrame sliceFrameAdvanced_; private SliceTiming sliceTiming_; private final MultiChannelSubPanel multiChannelPanel_; private final Color[] colors = { Color.RED, Color.GREEN, Color.BLUE, Color.MAGENTA, Color.PINK, Color.CYAN, Color.YELLOW, Color.ORANGE }; private String lastAcquisitionPath_; private String lastAcquisitionName_; private MMAcquisition acq_; private String[] channelNames_; private int nrRepeats_; // how many separate acquisitions to perform private boolean resetXaxisSpeed_; private final AcquisitionPanel acquisitionPanel_; private final JComponent[] simpleTimingComponents_; private final JPanel slicePanel_; private final JPanel slicePanelContainer_; private final JPanel lightSheetPanel_; private final JPanel normalPanel_; double zStepUm_; // hold onto local copy so we don't have to keep querying double xPositionUm_; // hold onto local copy so we don't have to keep querying double yPositionUm_; // hold onto local copy so we don't have to keep querying double zPositionUm_; // hold onto local copy so we don't have to keep querying private final JButton gridButton_; private final MMFrame gridFrame_; private final JPanel gridPanel_; private final JPanel gridXPanel_; private final JCheckBox useXGridCB_; private final JFormattedTextField gridXStartField_; private final JFormattedTextField gridXStopField_; private final JFormattedTextField gridXDeltaField_; private final JLabel gridXCount_; private final JPanel gridYPanel_; private final JCheckBox useYGridCB_; private final JFormattedTextField gridYStartField_; private final JFormattedTextField gridYStopField_; private final JFormattedTextField gridYDeltaField_; private final JLabel gridYCount_; private final JPanel gridZPanel_; private final JCheckBox useZGridCB_; private final JFormattedTextField gridZStartField_; private final JFormattedTextField gridZStopField_; private final JFormattedTextField gridZDeltaField_; private final JLabel gridZCount_; private final JPanel gridSettingsPanel_; private final JCheckBox clearYZGridCB_; private final JButton computeGridButton_; private static final int XYSTAGETIMEOUT = 20000; public AcquisitionPanel(ScriptInterface gui, Devices devices, Properties props, Cameras cameras, Prefs prefs, StagePositionUpdater posUpdater, Positions positions, ControllerUtils controller, AutofocusUtils autofocus) { super(MyStrings.PanelNames.ACQUSITION.toString(), new MigLayout("", "[center]0[center]0[center]", "0[top]0")); gui_ = gui; devices_ = devices; props_ = props; cameras_ = cameras; prefs_ = prefs; posUpdater_ = posUpdater; positions_ = positions; controller_ = controller; autofocus_ = autofocus; core_ = gui_.getMMCore(); numTimePointsDone_ = 0; sliceTiming_ = new SliceTiming(); lastAcquisitionPath_ = ""; lastAcquisitionName_ = ""; acq_ = null; channelNames_ = null; resetXaxisSpeed_ = true; acquisitionPanel_ = this; PanelUtils pu = new PanelUtils(prefs_, props_, devices_); // added to spinner controls where we should re-calculate the displayed // slice period, volume duration, and time lapse duration ChangeListener recalculateTimingDisplayCL = new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { if (advancedSliceTimingCB_.isSelected()) { // need to update sliceTiming_ from property values sliceTiming_ = getTimingFromAdvancedSettings(); } updateDurationLabels(); } }; // added to combobox controls where we should re-calculate the displayed // slice period, volume duration, and time lapse duration ActionListener recalculateTimingDisplayAL = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { updateDurationLabels(); } }; // start volume sub-panel volPanel_ = new JPanel(new MigLayout("", "[right]10[center]", "4[]8[]")); volPanel_.setBorder(PanelUtils.makeTitledBorder("Volume Settings")); if (!ASIdiSPIM.oSPIM) { } else { props_.setPropValue(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_NUM_SIDES, "1"); } volPanel_.add(new JLabel("Number of sides:")); String[] str12 = { "1", "2" }; numSides_ = pu.makeDropDownBox(str12, Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_NUM_SIDES, str12[1]); numSides_.addActionListener(recalculateTimingDisplayAL); if (!ASIdiSPIM.oSPIM) { } else { numSides_.setEnabled(false); } volPanel_.add(numSides_, "wrap"); volPanel_.add(new JLabel("First side:")); String[] ab = { Devices.Sides.A.toString(), Devices.Sides.B.toString() }; if (!ASIdiSPIM.oSPIM) { } else { props_.setPropValue(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_FIRST_SIDE, Devices.Sides.A.toString()); } firstSide_ = pu.makeDropDownBox(ab, Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_FIRST_SIDE, Devices.Sides.A.toString()); firstSide_.addActionListener(recalculateTimingDisplayAL); if (!ASIdiSPIM.oSPIM) { } else { firstSide_.setEnabled(false); } volPanel_.add(firstSide_, "wrap"); volPanel_.add(new JLabel("Delay before side [ms]:")); // used to read/write directly to galvo/micro-mirror firmware, but want different stage scan behavior delaySide_ = pu.makeSpinnerFloat(0, 10000, 0.25, Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_DELAY_BEFORE_SIDE, 50); pu.addListenerLast(delaySide_, recalculateTimingDisplayCL); volPanel_.add(delaySide_, "wrap"); volPanel_.add(new JLabel("Slices per side:")); numSlices_ = pu.makeSpinnerInteger(1, 65000, Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_NUM_SLICES, 20); pu.addListenerLast(numSlices_, recalculateTimingDisplayCL); volPanel_.add(numSlices_, "wrap"); volPanel_.add(new JLabel("Slice step size [\u00B5m]:")); stepSize_ = pu.makeSpinnerFloat(0, 100, 0.1, Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_SLICE_STEP_SIZE, 1.0); pu.addListenerLast(stepSize_, recalculateTimingDisplayCL); // needed only for stage scanning b/c acceleration time related to speed volPanel_.add(stepSize_, "wrap"); // end volume sub-panel // start slice timing controls, have 2 options with advanced timing checkbox shared slicePanel_ = new JPanel(new MigLayout("", "[right]10[center]", "0[]0[]")); slicePanel_.setBorder(PanelUtils.makeTitledBorder("Slice Settings")); // start light sheet controls lightSheetPanel_ = new JPanel(new MigLayout("", "[right]10[center]", "4[]8")); lightSheetPanel_.add(new JLabel("Scan reset time [ms]:")); JSpinner lsScanReset = pu.makeSpinnerFloat(1, 100, 0.25, // practical lower limit of 1ms Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_LS_SCAN_RESET, 3); lsScanReset.addChangeListener(PanelUtils.coerceToQuarterIntegers(lsScanReset)); pu.addListenerLast(lsScanReset, recalculateTimingDisplayCL); lightSheetPanel_.add(lsScanReset, "wrap"); lightSheetPanel_.add(new JLabel("Scan settle time [ms]:")); JSpinner lsScanSettle = pu.makeSpinnerFloat(0.25, 100, 0.25, Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_LS_SCAN_SETTLE, 1); lsScanSettle.addChangeListener(PanelUtils.coerceToQuarterIntegers(lsScanSettle)); pu.addListenerLast(lsScanSettle, recalculateTimingDisplayCL); lightSheetPanel_.add(lsScanSettle, "wrap"); lightSheetPanel_.add(new JLabel("Shutter width [\u00B5m]:")); JSpinner lsShutterWidth = pu.makeSpinnerFloat(0.1, 100, 1, Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_LS_SHUTTER_WIDTH, 5); pu.addListenerLast(lsShutterWidth, recalculateTimingDisplayCL); lightSheetPanel_.add(lsShutterWidth); // lightSheetPanel_.add(new JLabel("1 / (shutter speed):")); // JSpinner lsShutterSpeed = pu.makeSpinnerInteger(1, 10, // Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_LS_SHUTTER_SPEED, 1); // lightSheetPanel_.add(lsShutterSpeed, "wrap"); // end light sheet controls // start "normal" (not light sheet) controls normalPanel_ = new JPanel(new MigLayout("", "[right]10[center]", "4[]8")); // out of order so we can reference it desiredSlicePeriod_ = pu.makeSpinnerFloat(1, 1000, 0.25, Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_DESIRED_SLICE_PERIOD, 30); minSlicePeriodCB_ = pu.makeCheckBox("Minimize slice period", Properties.Keys.PREFS_MINIMIZE_SLICE_PERIOD, panelName_, false); minSlicePeriodCB_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { boolean doMin = minSlicePeriodCB_.isSelected(); desiredSlicePeriod_.setEnabled(!doMin); desiredSlicePeriodLabel_.setEnabled(!doMin); recalculateSliceTiming(false); } }); normalPanel_.add(minSlicePeriodCB_, "span 2, wrap"); // special field that is enabled/disabled depending on whether advanced timing is enabled desiredSlicePeriodLabel_ = new JLabel("Slice period [ms]:"); normalPanel_.add(desiredSlicePeriodLabel_); normalPanel_.add(desiredSlicePeriod_, "wrap"); desiredSlicePeriod_.addChangeListener(PanelUtils.coerceToQuarterIntegers(desiredSlicePeriod_)); desiredSlicePeriod_.addChangeListener(recalculateTimingDisplayCL); // special field that is enabled/disabled depending on whether advanced timing is enabled desiredLightExposureLabel_ = new JLabel("Sample exposure [ms]:"); normalPanel_.add(desiredLightExposureLabel_); desiredLightExposure_ = pu.makeSpinnerFloat(1.0, 1000, 0.25, Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_DESIRED_EXPOSURE, 8.5); desiredLightExposure_.addChangeListener(PanelUtils.coerceToQuarterIntegers(desiredLightExposure_)); desiredLightExposure_.addChangeListener(recalculateTimingDisplayCL); normalPanel_.add(desiredLightExposure_); // end normal simple slice timing controls slicePanelContainer_ = new JPanel(new MigLayout("", "0[center]0", "0[]0")); slicePanelContainer_.add( getSPIMCameraMode() == CameraModes.Keys.LIGHT_SHEET ? lightSheetPanel_ : normalPanel_, "growx"); slicePanel_.add(slicePanelContainer_, "span 2, center, wrap"); // special checkbox to use the advanced timing settings // action handler added below after defining components it enables/disables advancedSliceTimingCB_ = pu.makeCheckBox("Use advanced timing settings", Properties.Keys.PREFS_ADVANCED_SLICE_TIMING, panelName_, false); slicePanel_.add(advancedSliceTimingCB_, "span 2, left"); // end slice sub-panel // start advanced slice timing frame // visibility of this frame is controlled from advancedTiming checkbox // this frame is separate from main plugin window sliceFrameAdvanced_ = new MMFrame(); sliceFrameAdvanced_.setTitle("Advanced timing"); sliceFrameAdvanced_.loadPosition(100, 100); sliceAdvancedPanel_ = new JPanel(new MigLayout("", "[right]10[center]", "[]8[]")); sliceFrameAdvanced_.add(sliceAdvancedPanel_); class SliceFrameAdapter extends WindowAdapter { @Override public void windowClosing(WindowEvent e) { advancedSliceTimingCB_.setSelected(false); sliceFrameAdvanced_.savePosition(); } } sliceFrameAdvanced_.addWindowListener(new SliceFrameAdapter()); JLabel scanDelayLabel = new JLabel("Delay before scan [ms]:"); sliceAdvancedPanel_.add(scanDelayLabel); delayScan_ = pu.makeSpinnerFloat(0, 10000, 0.25, new Devices.Keys[] { Devices.Keys.GALVOA, Devices.Keys.GALVOB }, Properties.Keys.SPIM_DELAY_SCAN, 0); delayScan_.addChangeListener(PanelUtils.coerceToQuarterIntegers(delayScan_)); delayScan_.addChangeListener(recalculateTimingDisplayCL); sliceAdvancedPanel_.add(delayScan_, "wrap"); JLabel lineScanLabel = new JLabel("Lines scans per slice:"); sliceAdvancedPanel_.add(lineScanLabel); numScansPerSlice_ = pu.makeSpinnerInteger(1, 1000, new Devices.Keys[] { Devices.Keys.GALVOA, Devices.Keys.GALVOB }, Properties.Keys.SPIM_NUM_SCANSPERSLICE, 1); numScansPerSlice_.addChangeListener(recalculateTimingDisplayCL); sliceAdvancedPanel_.add(numScansPerSlice_, "wrap"); JLabel lineScanPeriodLabel = new JLabel("Line scan duration [ms]:"); sliceAdvancedPanel_.add(lineScanPeriodLabel); lineScanDuration_ = pu.makeSpinnerFloat(1, 10000, 0.25, new Devices.Keys[] { Devices.Keys.GALVOA, Devices.Keys.GALVOB }, Properties.Keys.SPIM_DURATION_SCAN, 10); lineScanDuration_.addChangeListener(PanelUtils.coerceToQuarterIntegers(lineScanDuration_)); lineScanDuration_.addChangeListener(recalculateTimingDisplayCL); sliceAdvancedPanel_.add(lineScanDuration_, "wrap"); JLabel delayLaserLabel = new JLabel("Delay before laser [ms]:"); sliceAdvancedPanel_.add(delayLaserLabel); delayLaser_ = pu.makeSpinnerFloat(0, 10000, 0.25, new Devices.Keys[] { Devices.Keys.GALVOA, Devices.Keys.GALVOB }, Properties.Keys.SPIM_DELAY_LASER, 0); delayLaser_.addChangeListener(PanelUtils.coerceToQuarterIntegers(delayLaser_)); delayLaser_.addChangeListener(recalculateTimingDisplayCL); sliceAdvancedPanel_.add(delayLaser_, "wrap"); JLabel durationLabel = new JLabel("Laser trig duration [ms]:"); sliceAdvancedPanel_.add(durationLabel); durationLaser_ = pu.makeSpinnerFloat(0, 10000, 0.25, new Devices.Keys[] { Devices.Keys.GALVOA, Devices.Keys.GALVOB }, Properties.Keys.SPIM_DURATION_LASER, 1); durationLaser_.addChangeListener(PanelUtils.coerceToQuarterIntegers(durationLaser_)); durationLaser_.addChangeListener(recalculateTimingDisplayCL); sliceAdvancedPanel_.add(durationLaser_, "span 2, wrap"); JLabel delayLabel = new JLabel("Delay before camera [ms]:"); sliceAdvancedPanel_.add(delayLabel); delayCamera_ = pu.makeSpinnerFloat(0, 10000, 0.25, new Devices.Keys[] { Devices.Keys.GALVOA, Devices.Keys.GALVOB }, Properties.Keys.SPIM_DELAY_CAMERA, 0); delayCamera_.addChangeListener(PanelUtils.coerceToQuarterIntegers(delayCamera_)); delayCamera_.addChangeListener(recalculateTimingDisplayCL); sliceAdvancedPanel_.add(delayCamera_, "wrap"); JLabel cameraLabel = new JLabel("Camera trig duration [ms]:"); sliceAdvancedPanel_.add(cameraLabel); durationCamera_ = pu.makeSpinnerFloat(0, 1000, 0.25, new Devices.Keys[] { Devices.Keys.GALVOA, Devices.Keys.GALVOB }, Properties.Keys.SPIM_DURATION_CAMERA, 0); durationCamera_.addChangeListener(PanelUtils.coerceToQuarterIntegers(durationCamera_)); durationCamera_.addChangeListener(recalculateTimingDisplayCL); sliceAdvancedPanel_.add(durationCamera_, "wrap"); JLabel exposureLabel = new JLabel("Camera exposure [ms]:"); sliceAdvancedPanel_.add(exposureLabel); exposureCamera_ = pu.makeSpinnerFloat(0, 1000, 0.25, Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_ADVANCED_CAMERA_EXPOSURE, 10f); exposureCamera_.addChangeListener(recalculateTimingDisplayCL); sliceAdvancedPanel_.add(exposureCamera_, "wrap"); alternateBeamScanCB_ = pu.makeCheckBox("Alternate scan direction", Properties.Keys.PREFS_SCAN_OPPOSITE_DIRECTIONS, panelName_, false); sliceAdvancedPanel_.add(alternateBeamScanCB_, "center, span 2, wrap"); simpleTimingComponents_ = new JComponent[] { desiredLightExposure_, minSlicePeriodCB_, desiredSlicePeriodLabel_, desiredLightExposureLabel_ }; final JComponent[] advancedTimingComponents = { delayScan_, numScansPerSlice_, lineScanDuration_, delayLaser_, durationLaser_, delayCamera_, durationCamera_, exposureCamera_, alternateBeamScanCB_ }; PanelUtils.componentsSetEnabled(advancedTimingComponents, advancedSliceTimingCB_.isSelected()); PanelUtils.componentsSetEnabled(simpleTimingComponents_, !advancedSliceTimingCB_.isSelected()); // this action listener takes care of enabling/disabling inputs // of the advanced slice timing window // we call this to get GUI looking right ItemListener sliceTimingDisableGUIInputs = new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { boolean enabled = advancedSliceTimingCB_.isSelected(); // set other components in this advanced timing frame PanelUtils.componentsSetEnabled(advancedTimingComponents, enabled); // also control some components in main volume settings sub-panel PanelUtils.componentsSetEnabled(simpleTimingComponents_, !enabled); desiredSlicePeriod_.setEnabled(!enabled && !minSlicePeriodCB_.isSelected()); desiredSlicePeriodLabel_.setEnabled(!enabled && !minSlicePeriodCB_.isSelected()); updateDurationLabels(); } }; // this action listener shows/hides the advanced timing frame ActionListener showAdvancedTimingFrame = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { boolean enabled = advancedSliceTimingCB_.isSelected(); if (enabled) { sliceFrameAdvanced_.setVisible(enabled); } } }; sliceFrameAdvanced_.pack(); sliceFrameAdvanced_.setResizable(false); // end slice Frame // start repeat (time lapse) sub-panel timepointPanel_ = new JPanel(new MigLayout("", "[right]10[center]", "[]8[]")); useTimepointsCB_ = pu.makeCheckBox("Time points", Properties.Keys.PREFS_USE_TIMEPOINTS, panelName_, false); useTimepointsCB_.setToolTipText("Perform a time-lapse acquisition"); useTimepointsCB_.setEnabled(true); useTimepointsCB_.setFocusPainted(false); ComponentTitledBorder componentBorder = new ComponentTitledBorder(useTimepointsCB_, timepointPanel_, BorderFactory.createLineBorder(ASIdiSPIM.borderColor)); timepointPanel_.setBorder(componentBorder); ChangeListener recalculateTimeLapseDisplay = new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { updateActualTimeLapseDurationLabel(); } }; useTimepointsCB_.addChangeListener(recalculateTimeLapseDisplay); timepointPanel_.add(new JLabel("Number:")); numTimepoints_ = pu.makeSpinnerInteger(1, 100000, Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_NUM_ACQUISITIONS, 1); numTimepoints_.addChangeListener(recalculateTimeLapseDisplay); numTimepoints_.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent arg0) { // update nrRepeats_ variable so the acquisition can be extended or shortened // as long as we have separate timepoints if (acquisitionRunning_.get() && getSavingSeparateFile()) { nrRepeats_ = getNumTimepoints(); } } }); timepointPanel_.add(numTimepoints_, "wrap"); timepointPanel_.add(new JLabel("Interval [s]:")); acquisitionInterval_ = pu.makeSpinnerFloat(0.1, 32000, 0.1, Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_ACQUISITION_INTERVAL, 60); acquisitionInterval_.addChangeListener(recalculateTimeLapseDisplay); timepointPanel_.add(acquisitionInterval_, "wrap"); // enable/disable panel elements depending on checkbox state useTimepointsCB_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { PanelUtils.componentsSetEnabled(timepointPanel_, useTimepointsCB_.isSelected()); } }); PanelUtils.componentsSetEnabled(timepointPanel_, useTimepointsCB_.isSelected()); // initialize // end repeat sub-panel // start savePanel // TODO for now these settings aren't part of acquisition settings // TODO consider whether that should be changed final int textFieldWidth = 16; savePanel_ = new JPanel(new MigLayout("", "[right]10[center]8[left]", "[]4[]")); savePanel_.setBorder(PanelUtils.makeTitledBorder("Data Saving Settings")); separateTimePointsCB_ = pu.makeCheckBox("Separate viewer / file for each time point", Properties.Keys.PREFS_SEPARATE_VIEWERS_FOR_TIMEPOINTS, panelName_, false); saveCB_ = pu.makeCheckBox("Save while acquiring", Properties.Keys.PREFS_SAVE_WHILE_ACQUIRING, panelName_, false); // make sure that when separate viewer is enabled then saving gets enabled too separateTimePointsCB_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (separateTimePointsCB_.isSelected() && !saveCB_.isSelected()) { saveCB_.doClick(); // setSelected() won't work because need to call its listener } } }); savePanel_.add(separateTimePointsCB_, "span 3, left, wrap"); savePanel_.add(saveCB_, "skip 1, span 2, center, wrap"); JLabel dirRootLabel = new JLabel("Directory root:"); savePanel_.add(dirRootLabel); DefaultFormatter formatter = new DefaultFormatter(); formatter.setOverwriteMode(false); rootField_ = new JFormattedTextField(formatter); rootField_.setText(prefs_.getString(panelName_, Properties.Keys.PLUGIN_DIRECTORY_ROOT, "")); rootField_.addPropertyChangeListener(new PropertyChangeListener() { // will respond to commitEdit() as well as GUI edit on commit @Override public void propertyChange(PropertyChangeEvent evt) { prefs_.putString(panelName_, Properties.Keys.PLUGIN_DIRECTORY_ROOT, rootField_.getText()); } }); rootField_.setColumns(textFieldWidth); savePanel_.add(rootField_, "span 2"); JButton browseRootButton = new JButton(); browseRootButton.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { setRootDirectory(rootField_); prefs_.putString(panelName_, Properties.Keys.PLUGIN_DIRECTORY_ROOT, rootField_.getText()); } }); browseRootButton.setMargin(new Insets(2, 5, 2, 5)); browseRootButton.setText("..."); savePanel_.add(browseRootButton, "wrap"); JLabel namePrefixLabel = new JLabel(); namePrefixLabel.setText("Name prefix:"); savePanel_.add(namePrefixLabel); prefixField_ = new JFormattedTextField(formatter); prefixField_.setText(prefs_.getString(panelName_, Properties.Keys.PLUGIN_NAME_PREFIX, "acq")); prefixField_.addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { prefs_.putString(panelName_, Properties.Keys.PLUGIN_NAME_PREFIX, prefixField_.getText()); } }); prefixField_.setColumns(textFieldWidth); savePanel_.add(prefixField_, "span 2, wrap"); // since we use the name field even for acquisitions in RAM, // we only need to gray out the directory-related components final JComponent[] saveComponents = { browseRootButton, rootField_, dirRootLabel }; PanelUtils.componentsSetEnabled(saveComponents, saveCB_.isSelected()); saveCB_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { PanelUtils.componentsSetEnabled(saveComponents, saveCB_.isSelected()); } }); // end save panel // start duration report panel durationPanel_ = new JPanel(new MigLayout("", "[right]6[left, 40%!]", "[]5[]")); durationPanel_.setBorder(PanelUtils.makeTitledBorder("Durations")); durationPanel_.setPreferredSize(new Dimension(125, 0)); // fix width so it doesn't constantly change depending on text durationPanel_.add(new JLabel("Slice:")); actualSlicePeriodLabel_ = new JLabel(); durationPanel_.add(actualSlicePeriodLabel_, "wrap"); durationPanel_.add(new JLabel("Volume:")); actualVolumeDurationLabel_ = new JLabel(); durationPanel_.add(actualVolumeDurationLabel_, "wrap"); durationPanel_.add(new JLabel("Total:")); actualTimeLapseDurationLabel_ = new JLabel(); durationPanel_.add(actualTimeLapseDurationLabel_, "wrap"); // end duration report panel buttonTestAcq_ = new JButton("Test Acquisition"); buttonTestAcq_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { runTestAcquisition(Devices.Sides.NONE); } }); buttonStart_ = new JToggleButton(); buttonStart_.setIconTextGap(6); buttonStart_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (isAcquisitionRequested()) { stopAcquisition(); } else { runAcquisition(); } } }); updateStartButton(); // call once to initialize, isSelected() will be false // make the size of the test button match the start button (easier on the eye) Dimension sizeStart = buttonStart_.getPreferredSize(); Dimension sizeTest = buttonTestAcq_.getPreferredSize(); sizeTest.height = sizeStart.height; buttonTestAcq_.setPreferredSize(sizeTest); acquisitionStatusLabel_ = new JLabel(""); acquisitionStatusLabel_.setBackground(prefixField_.getBackground()); acquisitionStatusLabel_.setOpaque(true); updateAcquisitionStatus(AcquisitionStatus.NONE); // Channel Panel (separate file for code) multiChannelPanel_ = new MultiChannelSubPanel(gui, devices_, props_, prefs_); multiChannelPanel_.addDurationLabelListener(this); // Position Panel final JPanel positionPanel = new JPanel(); positionPanel.setLayout(new MigLayout("flowx, fillx", "[right]10[left][10][]", "[]8[]")); usePositionsCB_ = pu.makeCheckBox("Multiple positions (XY)", Properties.Keys.PREFS_USE_MULTIPOSITION, panelName_, false); usePositionsCB_.setToolTipText("Acquire datasest at multiple postions"); usePositionsCB_.setEnabled(true); usePositionsCB_.setFocusPainted(false); componentBorder = new ComponentTitledBorder(usePositionsCB_, positionPanel, BorderFactory.createLineBorder(ASIdiSPIM.borderColor)); positionPanel.setBorder(componentBorder); usePositionsCB_.addChangeListener(recalculateTimingDisplayCL); final JButton editPositionListButton = new JButton("Edit position list..."); editPositionListButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { gui_.showXYPositionList(); } }); positionPanel.add(editPositionListButton); gridButton_ = new JButton("XYZ grid..."); positionPanel.add(gridButton_, "wrap"); // start XYZ grid frame // visibility of this frame is controlled from XYZ grid button // this frame is separate from main plugin window gridXPanel_ = new JPanel(new MigLayout("", "[right]10[center]", "[]8[]")); useXGridCB_ = pu.makeCheckBox("Slices from stage coordinates", Properties.Keys.PREFS_USE_X_GRID, panelName_, true); useXGridCB_.setEnabled(true); useXGridCB_.setFocusPainted(false); componentBorder = new ComponentTitledBorder(useXGridCB_, gridXPanel_, BorderFactory.createLineBorder(ASIdiSPIM.borderColor)); gridXPanel_.setBorder(componentBorder); // enable/disable panel elements depending on checkbox state useXGridCB_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { PanelUtils.componentsSetEnabled(gridXPanel_, useXGridCB_.isSelected()); } }); gridXPanel_.add(new JLabel("X start [um]:")); gridXStartField_ = pu.makeFloatEntryField(panelName_, "Grid_X_Start", -400, 5); gridXStartField_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { updateGridXCount(); } }); gridXPanel_.add(gridXStartField_); JButton tmp_but = new JButton("Set"); tmp_but.setBackground(Color.red); tmp_but.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { gridXStartField_.setValue(positions_.getUpdatedPosition(Devices.Keys.XYSTAGE, Directions.X)); updateGridXCount(); } }); gridXPanel_.add(tmp_but, "wrap"); gridXPanel_.add(new JLabel("X stop [um]:")); gridXStopField_ = pu.makeFloatEntryField(panelName_, "Grid_X_Stop", 400, 5); gridXStopField_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { updateGridXCount(); } }); gridXPanel_.add(gridXStopField_); tmp_but = new JButton("Set"); tmp_but.setBackground(Color.red); tmp_but.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { gridXStopField_.setValue(positions_.getUpdatedPosition(Devices.Keys.XYSTAGE, Directions.X)); updateGridXCount(); } }); gridXPanel_.add(tmp_but, "wrap"); gridXPanel_.add(new JLabel("X delta [um]:")); gridXDeltaField_ = pu.makeFloatEntryField(panelName_, "Grid_X_Delta", 3, 5); gridXDeltaField_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { updateGridXCount(); } }); gridXPanel_.add(gridXDeltaField_, "wrap"); // tmp_but = new JButton("Set"); // tmp_but.setBackground(Color.red); // tmp_but.addActionListener(new ActionListener() { // @Override // public void actionPerformed(ActionEvent arg0) { // // TODO figure out spacing, maybe to make reslicing trivial // updateGridXCount(); // } // }); // gridPanel_.add(tmp_but, "wrap"); gridXPanel_.add(new JLabel("Slice count:")); gridXCount_ = new JLabel(""); gridXPanel_.add(gridXCount_, "wrap"); updateGridXCount(); PanelUtils.componentsSetEnabled(gridXPanel_, useXGridCB_.isSelected()); // initialize gridYPanel_ = new JPanel(new MigLayout("", "[right]10[center]", "[]8[]")); useYGridCB_ = pu.makeCheckBox("Grid in Y", Properties.Keys.PREFS_USE_Y_GRID, panelName_, true); useYGridCB_.setEnabled(true); useYGridCB_.setFocusPainted(false); componentBorder = new ComponentTitledBorder(useYGridCB_, gridYPanel_, BorderFactory.createLineBorder(ASIdiSPIM.borderColor)); gridYPanel_.setBorder(componentBorder); // enable/disable panel elements depending on checkbox state useYGridCB_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { PanelUtils.componentsSetEnabled(gridYPanel_, useYGridCB_.isSelected()); } }); gridYPanel_.add(new JLabel("Y start [um]:")); gridYStartField_ = pu.makeFloatEntryField(panelName_, "Grid_Y_Start", -1200, 5); gridYStartField_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { updateGridYCount(); } }); gridYPanel_.add(gridYStartField_); tmp_but = new JButton("Set"); tmp_but.setBackground(Color.red); tmp_but.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { gridYStartField_.setValue(positions_.getUpdatedPosition(Devices.Keys.XYSTAGE, Directions.Y)); updateGridYCount(); } }); gridYPanel_.add(tmp_but, "wrap"); gridYPanel_.add(new JLabel("Y stop [um]:")); gridYStopField_ = pu.makeFloatEntryField(panelName_, "Grid_Y_Stop", 1200, 5); gridYStopField_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { updateGridYCount(); } }); gridYPanel_.add(gridYStopField_); tmp_but = new JButton("Set"); tmp_but.setBackground(Color.red); tmp_but.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { gridYStopField_.setValue(positions_.getUpdatedPosition(Devices.Keys.XYSTAGE, Directions.Y)); updateGridYCount(); } }); gridYPanel_.add(tmp_but, "wrap"); gridYPanel_.add(new JLabel("Y delta [um]:")); gridYDeltaField_ = pu.makeFloatEntryField(panelName_, "Grid_Y_Delta", 700, 5); gridYDeltaField_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { updateGridYCount(); } }); gridYPanel_.add(gridYDeltaField_); tmp_but = new JButton("Set"); tmp_but.setBackground(Color.red); tmp_but.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { Devices.Keys camKey = isFirstSideA() ? Devices.Keys.CAMERAA : Devices.Keys.CAMERAB; int height; try { height = core_.getROI(devices_.getMMDevice(camKey)).height; } catch (Exception e) { height = 1; } float pixelSize = (float) core_.getPixelSizeUm(); double delta = height * pixelSize; double overlap = props_.getPropValueFloat(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_GRID_OVERLAP_PERCENT); delta *= (1 - overlap / 100); // sanity checks, would be better handled with exceptions or more formal checks if (height > 4100 || height < 4 || pixelSize < 1e-6) { return; } gridYDeltaField_.setValue(Math.round(delta)); updateGridYCount(); } }); gridYPanel_.add(tmp_but, "wrap"); gridYPanel_.add(new JLabel("Y count:")); gridYCount_ = new JLabel(""); gridYPanel_.add(gridYCount_, "wrap"); updateGridYCount(); PanelUtils.componentsSetEnabled(gridYPanel_, useYGridCB_.isSelected()); // initialize gridZPanel_ = new JPanel(new MigLayout("", "[right]10[center]", "[]8[]")); useZGridCB_ = pu.makeCheckBox("Grid in Z", Properties.Keys.PREFS_USE_Z_GRID, panelName_, true); useZGridCB_.setEnabled(true); useZGridCB_.setFocusPainted(false); componentBorder = new ComponentTitledBorder(useZGridCB_, gridZPanel_, BorderFactory.createLineBorder(ASIdiSPIM.borderColor)); gridZPanel_.setBorder(componentBorder); // enable/disable panel elements depending on checkbox state useZGridCB_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { PanelUtils.componentsSetEnabled(gridZPanel_, useZGridCB_.isSelected()); } }); gridZPanel_.add(new JLabel("Z start [um]:")); gridZStartField_ = pu.makeFloatEntryField(panelName_, "Grid_Z_Start", 0, 5); gridZStartField_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { updateGridZCount(); } }); gridZPanel_.add(gridZStartField_); tmp_but = new JButton("Set"); tmp_but.setBackground(Color.red); tmp_but.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { gridZStartField_.setValue(positions_.getUpdatedPosition(Devices.Keys.UPPERZDRIVE)); updateGridZCount(); } }); gridZPanel_.add(tmp_but, "wrap"); gridZPanel_.add(new JLabel("Z stop [um]:")); gridZStopField_ = pu.makeFloatEntryField(panelName_, "Grid_Z_Stop", -800, 5); gridZStopField_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { updateGridZCount(); } }); gridZPanel_.add(gridZStopField_); tmp_but = new JButton("Set"); tmp_but.setBackground(Color.red); tmp_but.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { gridZStopField_.setValue(positions_.getUpdatedPosition(Devices.Keys.UPPERZDRIVE)); updateGridZCount(); } }); gridZPanel_.add(tmp_but, "wrap"); gridZPanel_.add(new JLabel("Z delta [um]:")); gridZDeltaField_ = pu.makeFloatEntryField(panelName_, "Grid_Z_Delta", 400, 5); gridZDeltaField_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { updateGridZCount(); } }); gridZPanel_.add(gridZDeltaField_); tmp_but = new JButton("Set"); tmp_but.setBackground(Color.red); tmp_but.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { Devices.Keys camKey = isFirstSideA() ? Devices.Keys.CAMERAA : Devices.Keys.CAMERAB; int width; try { width = core_.getROI(devices_.getMMDevice(camKey)).width; } catch (Exception e) { width = 1; } float pixelSize = (float) core_.getPixelSizeUm(); // sanity checks, would be better handled with exceptions or more formal checks if (width > 4100 || width < 4 || pixelSize < 1e-6) { return; } double delta = width * pixelSize / Math.sqrt(2); double overlap = props_.getPropValueFloat(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_GRID_OVERLAP_PERCENT); delta *= (1 - overlap / 100); gridZDeltaField_.setValue(Math.round(delta)); updateGridZCount(); } }); gridZPanel_.add(tmp_but, "wrap"); gridZPanel_.add(new JLabel("Z count:")); gridZCount_ = new JLabel(""); gridZPanel_.add(gridZCount_, "wrap"); updateGridZCount(); PanelUtils.componentsSetEnabled(gridZPanel_, useZGridCB_.isSelected()); // initialize gridSettingsPanel_ = new JPanel(new MigLayout("", "[right]10[center]", "[]8[]")); gridSettingsPanel_.setBorder(PanelUtils.makeTitledBorder("Grid settings")); gridSettingsPanel_.add(new JLabel("Overlap (Y and Z) [%]:")); JSpinner tileOverlapPercent = pu.makeSpinnerFloat(0, 100, 1, Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_GRID_OVERLAP_PERCENT, 10); gridSettingsPanel_.add(tileOverlapPercent, "wrap"); clearYZGridCB_ = pu.makeCheckBox("Clear position list if YZ unused", Properties.Keys.PREFS_CLEAR_YZ_GRID, panelName_, true); gridSettingsPanel_.add(clearYZGridCB_, "span 2"); computeGridButton_ = new JButton("Compute grid"); computeGridButton_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { final boolean useX = useXGridCB_.isSelected(); final boolean useY = useYGridCB_.isSelected(); final boolean useZ = useZGridCB_.isSelected(); final int numX = useX ? updateGridXCount() : 1; final int numY = useY ? updateGridYCount() : 1; final int numZ = useZ ? updateGridZCount() : 1; double centerX = (((Double) gridXStartField_.getValue()) + ((Double) gridXStopField_.getValue())) / 2; double centerY = (((Double) gridYStartField_.getValue()) + ((Double) gridYStopField_.getValue())) / 2; double centerZ = (((Double) gridZStartField_.getValue()) + ((Double) gridZStopField_.getValue())) / 2; double deltaX = (Double) gridXDeltaField_.getValue(); double deltaY = (Double) gridYDeltaField_.getValue(); double deltaZ = (Double) gridZDeltaField_.getValue(); double startY = centerY - deltaY * (numY - 1) / 2; double startZ = centerZ - deltaZ * (numZ - 1) / 2; String xy_device = devices_.getMMDevice(Devices.Keys.XYSTAGE); String z_device = devices_.getMMDevice(Devices.Keys.UPPERZDRIVE); if (useX) { try { setVolumeSliceStepSize(Math.abs(deltaX) / Math.sqrt(2)); setVolumeSlicesPerVolume(numX); if (!useY && !useZ) { // move to X center if we aren't generating position list with it positions_.setPosition(Devices.Keys.XYSTAGE, Directions.X, centerX); } } catch (Exception ex) { // not sure what to do in case of error so ignore } } else { // use current X value as center; this was original behavior centerX = positions_.getUpdatedPosition(Devices.Keys.XYSTAGE, Directions.X); } // if we aren't using one axis, use the current position instead of GUI position if (useY && !useZ) { startZ = positions_.getUpdatedPosition(Devices.Keys.UPPERZDRIVE); } if (useZ && !useY) { startY = positions_.getUpdatedPosition(Devices.Keys.XYSTAGE, Directions.Y); } if (!useY && !useZ && !clearYZGridCB_.isSelected()) { return; } PositionList pl; try { pl = gui_.getPositionList(); } catch (MMScriptException e) { pl = new PositionList(); } boolean isPositionListEmpty = pl.getNumberOfPositions() == 0; if (!isPositionListEmpty) { boolean overwrite = MyDialogUtils.getConfirmDialogResult( "Do you really want to overwrite the existing position list?", JOptionPane.YES_NO_OPTION); if (!overwrite) { return; // nothing to do } } pl = new PositionList(); if (useY || useZ) { for (int iZ = 0; iZ < numZ; ++iZ) { for (int iY = 0; iY < numY; ++iY) { MultiStagePosition msp = new MultiStagePosition(); StagePosition s = new StagePosition(); s.stageName = xy_device; s.numAxes = 2; s.x = centerX; s.y = startY + iY * deltaY; msp.add(s); StagePosition s2 = new StagePosition(); s2.stageName = z_device; s2.x = startZ + iZ * deltaZ; msp.add(s2); msp.setLabel("Pos_" + iZ + "_" + iY); pl.addPosition(msp); } } } try { gui_.setPositionList(pl); } catch (MMScriptException ex) { MyDialogUtils.showError(ex, "Couldn't overwrite position list with generated YZ grid"); } } }); gridFrame_ = new MMFrame(); gridFrame_.setTitle("XYZ Grid"); gridFrame_.loadPosition(100, 100); gridPanel_ = new JPanel(new MigLayout("", "[right]10[center]", "[]8[]")); gridFrame_.add(gridPanel_); class GridFrameAdapter extends WindowAdapter { @Override public void windowClosing(WindowEvent e) { gridButton_.setSelected(false); gridFrame_.savePosition(); } } gridFrame_.addWindowListener(new GridFrameAdapter()); gridButton_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { gridFrame_.setVisible(true); } }); gridPanel_.add(gridYPanel_); gridPanel_.add(gridZPanel_, "wrap"); gridPanel_.add(gridXPanel_, "spany 2"); gridPanel_.add(gridSettingsPanel_, "growx, wrap"); gridPanel_.add(computeGridButton_, "growx, growy"); gridFrame_.pack(); gridFrame_.setResizable(false); // end YZ grid frame positionPanel.add(new JLabel("Post-move delay [ms]:")); positionDelay_ = pu.makeSpinnerFloat(0.0, 10000.0, 100.0, Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_POSITION_DELAY, 0.0); positionPanel.add(positionDelay_, "wrap"); // enable/disable panel elements depending on checkbox state usePositionsCB_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { PanelUtils.componentsSetEnabled(positionPanel, usePositionsCB_.isSelected()); gridButton_.setEnabled(true); // leave this always enabled } }); PanelUtils.componentsSetEnabled(positionPanel, usePositionsCB_.isSelected()); // initialize gridButton_.setEnabled(true); // leave this always enabled // end of Position panel // checkbox to use navigation joystick settings or not // an "orphan" UI element navigationJoysticksCB_ = new JCheckBox("Use Navigation joystick settings"); navigationJoysticksCB_ .setSelected(prefs_.getBoolean(panelName_, Properties.Keys.PLUGIN_USE_NAVIGATION_JOYSTICKS, false)); navigationJoysticksCB_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { updateJoysticks(); prefs_.putBoolean(panelName_, Properties.Keys.PLUGIN_USE_NAVIGATION_JOYSTICKS, navigationJoysticksCB_.isSelected()); } }); // checkbox to signal that autofocus should be used during acquisition // another orphan UI element useAutofocusCB_ = new JCheckBox("Autofocus periodically"); useAutofocusCB_ .setSelected(prefs_.getBoolean(panelName_, Properties.Keys.PLUGIN_ACQUSITION_USE_AUTOFOCUS, false)); useAutofocusCB_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { prefs_.putBoolean(panelName_, Properties.Keys.PLUGIN_ACQUSITION_USE_AUTOFOCUS, useAutofocusCB_.isSelected()); } }); // checkbox to signal that movement should be corrected during acquisition // Yet another orphan UI element useMovementCorrectionCB_ = new JCheckBox("Motion correction"); useMovementCorrectionCB_.setSelected( prefs_.getBoolean(panelName_, Properties.Keys.PLUGIN_ACQUSITION_USE_MOVEMENT_CORRECTION, false)); useMovementCorrectionCB_.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { prefs_.putBoolean(panelName_, Properties.Keys.PLUGIN_ACQUSITION_USE_MOVEMENT_CORRECTION, useMovementCorrectionCB_.isSelected()); } }); // set up tabbed panels for GUI // make 3 columns as own JPanels to get vertical space right // in each column without dependencies on other columns leftColumnPanel_ = new JPanel(new MigLayout("", "[]", "[]6[]10[]10[]")); leftColumnPanel_.add(durationPanel_, "split 2"); leftColumnPanel_.add(timepointPanel_, "wrap, growx"); leftColumnPanel_.add(savePanel_, "wrap"); leftColumnPanel_.add(new JLabel("Acquisition mode: "), "split 2, right"); AcquisitionModes acqModes = new AcquisitionModes(devices_, prefs_); spimMode_ = acqModes.getComboBox(); spimMode_.addActionListener(recalculateTimingDisplayAL); leftColumnPanel_.add(spimMode_, "left, wrap"); leftColumnPanel_.add(buttonStart_, "split 3, left"); leftColumnPanel_.add(new JLabel(" ")); leftColumnPanel_.add(buttonTestAcq_, "wrap"); leftColumnPanel_.add(new JLabel("Status:"), "split 2, left"); leftColumnPanel_.add(acquisitionStatusLabel_); centerColumnPanel_ = new JPanel(new MigLayout("", "[]", "[]")); centerColumnPanel_.add(positionPanel, "growx, wrap"); centerColumnPanel_.add(multiChannelPanel_, "wrap"); centerColumnPanel_.add(navigationJoysticksCB_, "wrap"); centerColumnPanel_.add(useAutofocusCB_, "split 2"); centerColumnPanel_.add(useMovementCorrectionCB_); rightColumnPanel_ = new JPanel(new MigLayout("", "[center]0", "[]0[]")); rightColumnPanel_.add(volPanel_, "growx, wrap"); rightColumnPanel_.add(slicePanel_, "growx"); // add the column panels to the main panel super.add(leftColumnPanel_); super.add(centerColumnPanel_); super.add(rightColumnPanel_); // properly initialize the advanced slice timing advancedSliceTimingCB_.addItemListener(sliceTimingDisableGUIInputs); sliceTimingDisableGUIInputs.itemStateChanged(null); advancedSliceTimingCB_.addActionListener(showAdvancedTimingFrame); // included is calculating slice timing updateDurationLabels(); // update local variables zStepUm_ = PanelUtils.getSpinnerFloatValue(stepSize_); refreshXYZPositions(); }//end constructor private int updateGridXCount() { double range = ((Double) gridXStartField_.getValue()) - ((Double) gridXStopField_.getValue()); double delta = ((Double) gridXDeltaField_.getValue()); if (Math.signum(range) != Math.signum(delta)) { delta *= -1; gridXDeltaField_.setValue(delta); } Integer count = (Integer) ((int) Math.ceil(range / delta)) + 1; gridXCount_.setText(count.toString()); return count; } private int updateGridYCount() { double range = ((Double) gridYStartField_.getValue()) - ((Double) gridYStopField_.getValue()); double delta = ((Double) gridYDeltaField_.getValue()); if (Math.signum(range) != Math.signum(delta)) { delta *= -1; gridYDeltaField_.setValue(delta); } Integer count = (Integer) ((int) Math.ceil(range / delta)) + 1; gridYCount_.setText(count.toString()); return count; } private int updateGridZCount() { double range = ((Double) gridZStartField_.getValue()) - ((Double) gridZStopField_.getValue()); double delta = ((Double) gridZDeltaField_.getValue()); if (Math.signum(range) != Math.signum(delta)) { delta *= -1; gridZDeltaField_.setValue(delta); } Integer count = (Integer) ((int) Math.ceil(range / delta)) + 1; gridZCount_.setText(count.toString()); return count; } private void updateJoysticks() { if (ASIdiSPIM.getFrame() != null) { ASIdiSPIM.getFrame().getNavigationPanel().doJoystickSettings(navigationJoysticksCB_.isSelected()); } } public final void updateDurationLabels() { updateActualSlicePeriodLabel(); updateActualVolumeDurationLabel(); updateActualTimeLapseDurationLabel(); } private void updateCalibrationOffset(final Devices.Sides side, final AutofocusUtils.FocusResult score) { if (score.focusSuccess_) { double maxDelta = props_.getPropValueFloat(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_AUTOFOCUS_MAXOFFSETCHANGE); if (Math.abs(score.offsetDelta_) <= maxDelta) { ASIdiSPIM.getFrame().getSetupPanel(side).updateCalibrationOffset(score); } else { ReportingUtils.logMessage("autofocus successful for side " + side + " but offset change too much to automatically update"); } } } public SliceTiming getSliceTiming() { return sliceTiming_; } /** * Sets the acquisition name prefix programmatically. * Added so that name prefix can be changed from a script. * @param acqName */ public void setAcquisitionNamePrefix(String acqName) { prefixField_.setText(acqName); } private void updateStartButton() { boolean started = isAcquisitionRequested(); buttonStart_.setSelected(started); buttonStart_.setText(started ? "Stop Acquisition!" : "Start Acquisition!"); buttonStart_.setBackground(started ? Color.red : Color.green); buttonStart_.setIcon( started ? SwingResourceManager.getIcon(MMStudio.class, "/org/micromanager/icons/cancel.png") : SwingResourceManager.getIcon(MMStudio.class, "/org/micromanager/icons/arrow_right.png")); buttonTestAcq_.setEnabled(!started); } /** * @return CameraModes.Keys value from Camera panel * (edge, overlap, pseudo-overlap, light sheet) */ private CameraModes.Keys getSPIMCameraMode() { CameraModes.Keys val; try { val = ASIdiSPIM.getFrame().getSPIMCameraMode(); } catch (Exception ex) { val = CameraModes.getKeyFromPrefCode( prefs_.getInt(MyStrings.PanelNames.SETTINGS.toString(), Properties.Keys.PLUGIN_CAMERA_MODE, 1)); // default is edge } return val; } /** * convenience method to avoid having to regenerate acquisition settings */ private int getNumTimepoints() { if (useTimepointsCB_.isSelected()) { return (Integer) numTimepoints_.getValue(); } else { return 1; } } /** * convenience method to avoid having to regenerate acquisition settings * public for API use * @return Number of Sides */ public int getNumSides() { if (numSides_.getSelectedIndex() == 1) { return 2; } else { return 1; } } /** * convenience method to avoid having to regenerate acquisition settings * public for API use * @return true if the first side is side A */ public boolean isFirstSideA() { return ((String) firstSide_.getSelectedItem()).equals("A"); } /** * convenience method to avoid having to regenerate acquisition settings. * public for API use * @return Time between starts of acquisition when doing a time-lapse acquisition */ public double getTimepointInterval() { return PanelUtils.getSpinnerFloatValue(acquisitionInterval_); } /** * Gathers all current acquisition settings into dedicated POD object * @return */ public AcquisitionSettings getCurrentAcquisitionSettings() { AcquisitionSettings acqSettings = new AcquisitionSettings(); acqSettings.spimMode = getAcquisitionMode(); acqSettings.isStageScanning = (acqSettings.spimMode == AcquisitionModes.Keys.STAGE_SCAN || acqSettings.spimMode == AcquisitionModes.Keys.STAGE_SCAN_INTERLEAVED || acqSettings.spimMode == AcquisitionModes.Keys.STAGE_SCAN_UNIDIRECTIONAL); acqSettings.useTimepoints = useTimepointsCB_.isSelected(); acqSettings.numTimepoints = getNumTimepoints(); acqSettings.timepointInterval = getTimepointInterval(); acqSettings.useMultiPositions = usePositionsCB_.isSelected(); acqSettings.useChannels = multiChannelPanel_.isMultiChannel(); acqSettings.channelMode = multiChannelPanel_.getChannelMode(); acqSettings.numChannels = multiChannelPanel_.getNumChannels(); acqSettings.channels = multiChannelPanel_.getUsedChannels(); acqSettings.channelGroup = multiChannelPanel_.getChannelGroup(); acqSettings.useAutofocus = useAutofocusCB_.isSelected(); acqSettings.useMovementCorrection = useMovementCorrectionCB_.isSelected(); acqSettings.acquireBothCamerasSimultaneously = prefs_.getBoolean(MyStrings.PanelNames.SETTINGS.toString(), Properties.Keys.PLUGIN_ACQUIRE_BOTH_CAMERAS_SIMULT, false); acqSettings.numSides = getNumSides(); acqSettings.firstSideIsA = isFirstSideA(); acqSettings.delayBeforeSide = PanelUtils.getSpinnerFloatValue(delaySide_); acqSettings.numSlices = (Integer) numSlices_.getValue(); acqSettings.stepSizeUm = PanelUtils.getSpinnerFloatValue(stepSize_); acqSettings.minimizeSlicePeriod = minSlicePeriodCB_.isSelected(); acqSettings.desiredSlicePeriod = PanelUtils.getSpinnerFloatValue(desiredSlicePeriod_); acqSettings.desiredLightExposure = PanelUtils.getSpinnerFloatValue(desiredLightExposure_); acqSettings.centerAtCurrentZ = false; acqSettings.sliceTiming = sliceTiming_; acqSettings.cameraMode = getSPIMCameraMode(); acqSettings.hardwareTimepoints = false; // when running acquisition we check this and set to true if needed acqSettings.separateTimepoints = getSavingSeparateFile(); return acqSettings; } /** * gets the correct value for the slice timing's sliceDuration field * based on other values of slice timing * @param s * @return */ private float getSliceDuration(final SliceTiming s) { // slice duration is the max out of the scan time, laser time, and camera time return Math.max(Math.max(s.scanDelay + (s.scanPeriod * s.scanNum), // scan time s.laserDelay + s.laserDuration // laser time ), s.cameraDelay + s.cameraDuration // camera time ); } /** * gets the slice timing from advanced settings * (normally these advanced settings are read-only and we populate them * ourselves depending on the user's requests and our algorithm below) * @return */ private SliceTiming getTimingFromAdvancedSettings() { SliceTiming s = new SliceTiming(); s.scanDelay = PanelUtils.getSpinnerFloatValue(delayScan_); s.scanNum = (Integer) numScansPerSlice_.getValue(); s.scanPeriod = PanelUtils.getSpinnerFloatValue(lineScanDuration_); s.laserDelay = PanelUtils.getSpinnerFloatValue(delayLaser_); s.laserDuration = PanelUtils.getSpinnerFloatValue(durationLaser_); s.cameraDelay = PanelUtils.getSpinnerFloatValue(delayCamera_); s.cameraDuration = PanelUtils.getSpinnerFloatValue(durationCamera_); s.cameraExposure = PanelUtils.getSpinnerFloatValue(exposureCamera_); s.sliceDuration = getSliceDuration(s); return s; } /** * @param showWarnings true to warn user about needing to change slice period * @return */ private SliceTiming getTimingFromPeriodAndLightExposure(boolean showWarnings) { // uses algorithm Jon worked out in Octave code; each slice period goes like this: // 1. camera readout time (none if in overlap mode, 0.25ms in pseudo-overlap) // 2. any extra delay time // 3. camera reset time // 4. start scan 0.25ms before camera global exposure and shifted up in time to account for delay introduced by Bessel filter // 5. turn on laser as soon as camera global exposure, leave laser on for desired light exposure time // 7. end camera exposure in final 0.25ms, post-filter scan waveform also ends now final float scanLaserBufferTime = MyNumberUtils.roundToQuarterMs(0.25f); // below assumed to be multiple of 0.25ms final Color foregroundColorOK = Color.BLACK; final Color foregroundColorError = Color.RED; final Component elementToColor = desiredSlicePeriod_.getEditor().getComponent(0); SliceTiming s = new SliceTiming(); final float cameraResetTime = computeCameraResetTime(); // recalculate for safety, 0 for light sheet final float cameraReadoutTime = computeCameraReadoutTime(); // recalculate for safety, 0 for overlap // can we use acquisition settings directly? because they may be in flux final AcquisitionSettings acqSettings = getCurrentAcquisitionSettings(); final float cameraReadout_max = MyNumberUtils.ceilToQuarterMs(cameraReadoutTime); final float cameraReset_max = MyNumberUtils.ceilToQuarterMs(cameraResetTime); // we will wait cameraReadout_max before triggering camera, then wait another cameraReset_max for global exposure // this will also be in 0.25ms increment final float globalExposureDelay_max = cameraReadout_max + cameraReset_max; final float laserDuration = MyNumberUtils.roundToQuarterMs(acqSettings.desiredLightExposure); final float scanDuration = laserDuration + 2 * scanLaserBufferTime; // scan will be longer than laser by 0.25ms at both start and end // account for delay in scan position due to Bessel filter by starting the scan slightly earlier // than we otherwise would (Bessel filter selected b/c stretches out pulse without any ripples) // delay to start is (empirically) 0.07ms + 0.25/(freq in kHz) // delay to midpoint is empirically 0.38/(freq in kHz) // group delay for 5th-order bessel filter ~0.39/freq from theory and ~0.4/freq from IC datasheet final float scanFilterFreq = Math.max( props_.getPropValueFloat(Devices.Keys.GALVOA, Properties.Keys.SCANNER_FILTER_X), props_.getPropValueFloat(Devices.Keys.GALVOB, Properties.Keys.SCANNER_FILTER_X)); float scanDelayFilter = 0; if (scanFilterFreq != 0) { scanDelayFilter = MyNumberUtils.roundToQuarterMs(0.39f / scanFilterFreq); } // If the PLogic card is used, account for 0.25ms delay it introduces to // the camera and laser trigger signals => subtract 0.25ms from the scanner delay // (start scanner 0.25ms later than it would be otherwise) // this time-shift opposes the Bessel filter delay // scanDelayFilter won't be negative unless scanFilterFreq is more than 3kHz which shouldn't happen if (devices_.isValidMMDevice(Devices.Keys.PLOGIC)) { scanDelayFilter -= 0.25f; } s.scanDelay = globalExposureDelay_max - scanLaserBufferTime // start scan 0.25ms before camera's global exposure - scanDelayFilter; // start galvo moving early due to card's Bessel filter and delay of TTL signals via PLC s.scanNum = 1; s.scanPeriod = scanDuration; s.laserDelay = globalExposureDelay_max; // turn on laser as soon as camera's global exposure is reached s.laserDuration = laserDuration; s.cameraDelay = cameraReadout_max; // camera must readout last frame before triggering again final Devices.Keys camKey = isFirstSideA() ? Devices.Keys.CAMERAA : Devices.Keys.CAMERAB; final Devices.Libraries camLibrary = devices_.getMMDeviceLibrary(camKey); // figure out desired time for camera to be exposing (including reset time) // because both camera trigger and laser on occur on 0.25ms intervals (i.e. we may not // trigger the laser until 0.24ms after global exposure) use cameraReset_max // special adjustment for Photometrics cameras that possibly has extra clear time which is counted in reset time // but not in the camera exposure time final float actualCameraResetTime = (camLibrary == Devices.Libraries.PVCAM && props_.getPropValueString(camKey, Properties.Keys.PVCAM_CHIPNAME) .equals(Properties.Values.PRIME_95B_CHIPNAME)) ? (float) props_.getPropValueInteger(camKey, Properties.Keys.PVCAM_READOUT_TIME) / 1e6f : cameraResetTime; // everything but Photometrics Prime 95B final float cameraExposure = MyNumberUtils.ceilToQuarterMs(actualCameraResetTime) + laserDuration; switch (acqSettings.cameraMode) { case EDGE: s.cameraDuration = 1; // doesn't really matter, 1ms should be plenty fast yet easy to see for debugging s.cameraExposure = cameraExposure + 0.1f; // add 0.1ms as safety margin, may require adding an additional 0.25ms to slice // slight delay between trigger and actual exposure start // is included in exposure time for Hamamatsu and negligible for Andor and PCO cameras // ensure not to miss triggers by not being done with readout in time for next trigger, add 0.25ms if needed if (getSliceDuration(s) < (s.cameraExposure + cameraReadoutTime)) { ReportingUtils.logDebugMessage( "Added 0.25ms in edge-trigger mode to make sure camera exposure long enough without dropping frames"); s.cameraDelay += 0.25f; s.laserDelay += 0.25f; s.scanDelay += 0.25f; } break; case LEVEL: // AKA "bulb mode", TTL rising starts exposure, TTL falling ends it s.cameraDuration = MyNumberUtils.ceilToQuarterMs(cameraExposure); s.cameraExposure = 1; // doesn't really matter, controlled by TTL break; case OVERLAP: // only Hamamatsu or Andor s.cameraDuration = 1; // doesn't really matter, 1ms should be plenty fast yet easy to see for debugging s.cameraExposure = 1; // doesn't really matter, controlled by interval between triggers break; case PSEUDO_OVERLAP: // PCO or Photometrics, enforce 0.25ms between end exposure and start of next exposure by triggering camera 0.25ms into the slice s.cameraDuration = 1; // doesn't really matter, 1ms should be plenty fast yet easy to see for debugging if (null != camLibrary) { switch (camLibrary) { case PCOCAM: s.cameraExposure = getSliceDuration(s) - s.cameraDelay; // s.cameraDelay should be 0.25ms for PCO break; case PVCAM: s.cameraExposure = cameraExposure; break; default: MyDialogUtils.showError("Unknown camera library for pseudo-overlap calculations"); break; } } if (s.cameraDelay < 0.24f) { MyDialogUtils.showError("Camera delay should be at least 0.25ms for pseudo-overlap mode."); } break; case LIGHT_SHEET: // each slice period goes like this: // 1. scan reset time (use to add any extra settling time to the start of each slice) // 2. start scan, wait scan settle time // 3. trigger camera/laser when scan settle time elapses // 4. scan for total of exposure time plus readout time (total time some row is exposing) plus settle time plus extra 0.25ms to prevent artifacts // 5. laser turns on 0.25ms before camera trigger and stays on until exposure is ending // TODO revisit this after further experimentation s.cameraDuration = 1; // only need to trigger camera final float shutterWidth = props_.getPropValueFloat(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_LS_SHUTTER_WIDTH); final int shutterSpeed = 1; // props_.getPropValueInteger(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_LS_SHUTTER_SPEED); float pixelSize = (float) core_.getPixelSizeUm(); if (pixelSize < 1e-6) { // can't compare equality directly with floating point values so call < 1e-9 is zero or negative pixelSize = 0.1625f; // default to pixel size of 40x with sCMOS = 6.5um/40 } final double rowReadoutTime = getRowReadoutTime(); s.cameraExposure = (float) (rowReadoutTime * shutterWidth / pixelSize * shutterSpeed); final float totalExposure_max = MyNumberUtils .ceilToQuarterMs(cameraReadoutTime + s.cameraExposure + 0.05f); // 50-300us extra cushion time final float scanSettle = props_.getPropValueFloat(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_LS_SCAN_SETTLE); final float scanReset = props_.getPropValueFloat(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_LS_SCAN_RESET); s.scanDelay = scanReset - scanDelayFilter; s.scanPeriod = scanSettle + totalExposure_max + scanLaserBufferTime; s.cameraDelay = scanReset + scanSettle; s.laserDelay = s.cameraDelay - scanLaserBufferTime; // trigger laser just before camera to make sure it's on already s.laserDuration = totalExposure_max + scanLaserBufferTime; // laser will turn off as exposure is ending break; case INTERNAL: default: if (showWarnings) { MyDialogUtils.showError("Invalid camera mode"); } s.valid = false; break; } // fix corner case of negative calculated scanDelay if (s.scanDelay < 0) { s.cameraDelay -= s.scanDelay; s.laserDelay -= s.scanDelay; s.scanDelay = 0; // same as (-= s.scanDelay) } // if a specific slice period was requested, add corresponding delay to scan/laser/camera elementToColor.setForeground(foregroundColorOK); if (!acqSettings.minimizeSlicePeriod) { float globalDelay = acqSettings.desiredSlicePeriod - getSliceDuration(s); // both should be in 0.25ms increments // TODO fix; if (acqSettings.cameraMode == CameraModes.Keys.LIGHT_SHEET) { globalDelay = 0; } if (globalDelay < 0) { globalDelay = 0; if (showWarnings) { // only true when user has specified period that is unattainable MyDialogUtils.showError("Increasing slice period to meet laser exposure constraint\n" + "(time required for camera readout; readout time depends on ROI)."); elementToColor.setForeground(foregroundColorError); } } s.scanDelay += globalDelay; s.cameraDelay += globalDelay; s.laserDelay += globalDelay; } // // Add 0.25ms to globalDelay if it is 0 and we are on overlap mode and scan has been shifted forward // // basically the last 0.25ms of scan time that would have determined the slice period isn't // // there any more because the scan time is moved up => add in the 0.25ms at the start of the slice // // in edge or level trigger mode the camera trig falling edge marks the end of the slice period // // not sure if PCO pseudo-overlap needs this, probably not because adding 0.25ms overhead in that case // if (MyNumberUtils.floatsEqual(cameraReadout_max, 0f) // true iff overlap being used // && (scanDelayFilter > 0.01f)) { // globalDelay += 0.25f; // } // fix corner case of (exposure time + readout time) being greater than the slice duration // most of the time the slice duration is already larger float globalDelay = MyNumberUtils .ceilToQuarterMs((s.cameraExposure + cameraReadoutTime) - getSliceDuration(s)); if (globalDelay > 0) { s.scanDelay += globalDelay; s.cameraDelay += globalDelay; s.laserDelay += globalDelay; } // update the slice duration based on our new values s.sliceDuration = getSliceDuration(s); return s; } /** * Re-calculate the controller's timing settings for "easy timing" mode. * Changes panel variable sliceTiming_. * The controller's properties will be set as needed * @param showWarnings will show warning if the user-specified slice period too short * or if cameras aren't assigned */ private void recalculateSliceTiming(boolean showWarnings) { if (!checkCamerasAssigned(showWarnings)) { return; } // if user is providing his own slice timing don't change it if (advancedSliceTimingCB_.isSelected()) { return; } sliceTiming_ = getTimingFromPeriodAndLightExposure(showWarnings); PanelUtils.setSpinnerFloatValue(delayScan_, sliceTiming_.scanDelay); numScansPerSlice_.setValue(sliceTiming_.scanNum); PanelUtils.setSpinnerFloatValue(lineScanDuration_, sliceTiming_.scanPeriod); PanelUtils.setSpinnerFloatValue(delayLaser_, sliceTiming_.laserDelay); PanelUtils.setSpinnerFloatValue(durationLaser_, sliceTiming_.laserDuration); PanelUtils.setSpinnerFloatValue(delayCamera_, sliceTiming_.cameraDelay); PanelUtils.setSpinnerFloatValue(durationCamera_, sliceTiming_.cameraDuration); PanelUtils.setSpinnerFloatValue(exposureCamera_, sliceTiming_.cameraExposure); } /** * Update the displayed slice period. */ private void updateActualSlicePeriodLabel() { recalculateSliceTiming(false); actualSlicePeriodLabel_.setText(NumberUtils.doubleToDisplayString(sliceTiming_.sliceDuration) + " ms"); } /** * calculate the total ramp time for stage scan in units of milliseconds (includes both acceleration and settling time * given by "delay before side" setting) * @param acqSettings * @return */ private double getStageRampDuration(AcquisitionSettings acqSettings) { final double accelerationX = controller_.computeScanAcceleration(controller_.computeScanSpeed(acqSettings)) + 1; // extra 1 for rounding up that often happens in controller ReportingUtils.logDebugMessage( "stage ramp duration is " + (acqSettings.delayBeforeSide + accelerationX) + " milliseconds"); return acqSettings.delayBeforeSide + accelerationX; } /** * calculate the retrace time in stage scan raster mode in units of milliseconds * @param acqSettings * @return */ private double getStageRetraceDuration(AcquisitionSettings acqSettings) { final double retraceSpeed = 0.67f * props_.getPropValueFloat(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_MAX_MOTOR_SPEED); // retrace speed set to 67% of max speed in firmware final double speedFactor = ASIdiSPIM.oSPIM ? (2 / Math.sqrt(3.)) : Math.sqrt(2.); final double scanDistance = acqSettings.numSlices * acqSettings.stepSizeUm * speedFactor; final double accelerationX = controller_.computeScanAcceleration(controller_.computeScanSpeed(acqSettings)) + 1; // extra 1 for rounding up that often happens in controller final double retraceDuration = scanDistance / retraceSpeed + accelerationX * 2; ReportingUtils.logDebugMessage("stage retrace duration is " + retraceDuration + " milliseconds"); return retraceDuration; } /** * Compute the volume duration in ms based on controller's timing settings. * Includes time for multiple channels. However, does not include for multiple positions. * @param acqSettings Settings for the acquisition * @return duration in ms */ public double computeActualVolumeDuration(AcquisitionSettings acqSettings) { final MultichannelModes.Keys channelMode = acqSettings.channelMode; final int numChannels = acqSettings.numChannels; final int numSides = acqSettings.numSides; final float delayBeforeSide = acqSettings.delayBeforeSide; int numCameraTriggers = acqSettings.numSlices; if (acqSettings.cameraMode == CameraModes.Keys.OVERLAP) { numCameraTriggers += 1; } // stackDuration is per-side, per-channel, per-position final double stackDuration = numCameraTriggers * acqSettings.sliceTiming.sliceDuration; if (acqSettings.isStageScanning) { final double rampDuration = getStageRampDuration(acqSettings); final double retraceTime = getStageRetraceDuration(acqSettings); // TODO double-check these calculations below, at least they are better than before ;-) if (acqSettings.spimMode == AcquisitionModes.Keys.STAGE_SCAN) { if (channelMode == MultichannelModes.Keys.SLICE_HW) { return retraceTime + (numSides * ((rampDuration * 2) + (stackDuration * numChannels))); } else { // "normal" stage scan with volume channel switching if (numSides == 1) { // single-view so will retrace at beginning of each channel return ((rampDuration * 2) + stackDuration + retraceTime) * numChannels; } else { // will only retrace at very start/end return retraceTime + (numSides * ((rampDuration * 2) + stackDuration) * numChannels); } } } else if (acqSettings.spimMode == AcquisitionModes.Keys.STAGE_SCAN_UNIDIRECTIONAL) { if (channelMode == MultichannelModes.Keys.SLICE_HW) { return ((rampDuration * 2) + (stackDuration * numChannels) + retraceTime) * numSides; } else { // "normal" stage scan with volume channel switching return ((rampDuration * 2) + stackDuration + retraceTime) * numChannels * numSides; } } else { // interleaved mode => one-way pass collecting both sides if (channelMode == MultichannelModes.Keys.SLICE_HW) { // single pass with all sides and channels return retraceTime + (rampDuration * 2 + stackDuration * numSides * numChannels); } else { // one-way pass collecting both sides, then rewind for next channel return ((rampDuration * 2) + (stackDuration * numSides) + retraceTime) * numChannels; } } } else { // piezo scan double channelSwitchDelay = 0; if (channelMode == MultichannelModes.Keys.VOLUME) { channelSwitchDelay = 500; // estimate channel switching overhead time as 0.5s // actual value will be hardware-dependent } if (channelMode == MultichannelModes.Keys.SLICE_HW) { return numSides * (delayBeforeSide + stackDuration * numChannels); // channelSwitchDelay = 0 } else { return numSides * numChannels * (delayBeforeSide + stackDuration) + (numChannels - 1) * channelSwitchDelay; } } } /** * Compute the timepoint duration in ms. Only difference from computeActualVolumeDuration() * is that it also takes into account the multiple positions, if any. * @return duration in ms */ private double computeTimepointDuration() { AcquisitionSettings acqSettings = getCurrentAcquisitionSettings(); final double volumeDuration = computeActualVolumeDuration(acqSettings); if (acqSettings.useMultiPositions) { try { // use 1.5 seconds motor move between positions // (could be wildly off but was estimated using actual system // and then slightly padded to be conservative to avoid errors // where positions aren't completed in time for next position) // could estimate the actual time by analyzing the position's relative locations // and using the motor speed and acceleration time return gui_.getPositionList().getNumberOfPositions() * (volumeDuration + 1500 + PanelUtils.getSpinnerFloatValue(positionDelay_)); } catch (MMScriptException ex) { MyDialogUtils.showError(ex, "Error getting position list for multiple XY positions"); } } return volumeDuration; } /** * Compute the volume duration in ms based on controller's timing settings. * Includes time for multiple channels. * @return duration in ms */ private double computeActualVolumeDuration() { return computeActualVolumeDuration(getCurrentAcquisitionSettings()); } /** * Update the displayed volume duration. */ private void updateActualVolumeDurationLabel() { double duration = computeActualVolumeDuration(); if (duration > 1000) { actualVolumeDurationLabel_.setText(NumberUtils.doubleToDisplayString(duration / 1000d) + " s"); // round to ms } else { actualVolumeDurationLabel_ .setText(NumberUtils.doubleToDisplayString(Math.round(10 * duration) / 10d) + " ms"); // round to tenth of ms } } /** * Compute the time lapse duration * @return duration in s */ private double computeActualTimeLapseDuration() { double duration = (getNumTimepoints() - 1) * getTimepointInterval() + computeTimepointDuration() / 1000; return duration; } /** * Update the displayed time lapse duration. */ private void updateActualTimeLapseDurationLabel() { String s = ""; double duration = computeActualTimeLapseDuration(); if (duration < 60) { // less than 1 min s += NumberUtils.doubleToDisplayString(duration) + " s"; } else if (duration < 60 * 60) { // between 1 min and 1 hour s += NumberUtils.doubleToDisplayString(Math.floor(duration / 60)) + " min "; s += NumberUtils.doubleToDisplayString(Math.round(duration % 60)) + " s"; } else { // longer than 1 hour s += NumberUtils.doubleToDisplayString(Math.floor(duration / (60 * 60))) + " hr "; s += NumberUtils.doubleToDisplayString(Math.round((duration % (60 * 60)) / 60)) + " min"; } actualTimeLapseDurationLabel_.setText(s); } /** * Computes the per-row readout time of the SPIM cameras set on Devices panel. * Handles single-side operation. * Needed for computing camera exposure in light sheet mode * @return */ private double getRowReadoutTime() { if (getNumSides() > 1) { return Math.max(cameras_.getRowReadoutTime(Devices.Keys.CAMERAA), cameras_.getRowReadoutTime(Devices.Keys.CAMERAB)); } else { if (isFirstSideA()) { return cameras_.getRowReadoutTime(Devices.Keys.CAMERAA); } else { return cameras_.getRowReadoutTime(Devices.Keys.CAMERAB); } } } /** * Computes the reset time of the SPIM cameras set on Devices panel. * Handles single-side operation. * Needed for computing (semi-)optimized slice timing in "easy timing" mode. * @return */ private float computeCameraResetTime() { CameraModes.Keys camMode = getSPIMCameraMode(); if (getNumSides() > 1) { return Math.max(cameras_.computeCameraResetTime(Devices.Keys.CAMERAA, camMode), cameras_.computeCameraResetTime(Devices.Keys.CAMERAB, camMode)); } else { if (isFirstSideA()) { return cameras_.computeCameraResetTime(Devices.Keys.CAMERAA, camMode); } else { return cameras_.computeCameraResetTime(Devices.Keys.CAMERAB, camMode); } } } /** * Computes the readout time of the SPIM cameras set on Devices panel. * Handles single-side operation. * Needed for computing (semi-)optimized slice timing in "easy timing" mode. * @return */ private float computeCameraReadoutTime() { CameraModes.Keys camMode = getSPIMCameraMode(); if (getNumSides() > 1) { return Math.max(cameras_.computeCameraReadoutTime(Devices.Keys.CAMERAA, camMode), cameras_.computeCameraReadoutTime(Devices.Keys.CAMERAB, camMode)); } else { if (isFirstSideA()) { return cameras_.computeCameraReadoutTime(Devices.Keys.CAMERAA, camMode); } else { return cameras_.computeCameraReadoutTime(Devices.Keys.CAMERAB, camMode); } } } /** * Makes sure that cameras are assigned to the desired sides and display error message * if not (e.g. if single-sided with side B first, then only checks camera for side B) * @return true if cameras assigned, false if not */ private boolean checkCamerasAssigned(boolean showWarnings) { String firstCamera, secondCamera; final boolean firstSideA = isFirstSideA(); if (firstSideA) { firstCamera = devices_.getMMDevice(Devices.Keys.CAMERAA); secondCamera = devices_.getMMDevice(Devices.Keys.CAMERAB); } else { firstCamera = devices_.getMMDevice(Devices.Keys.CAMERAB); secondCamera = devices_.getMMDevice(Devices.Keys.CAMERAA); } if (firstCamera == null) { if (showWarnings) { MyDialogUtils.showError("Please select a valid camera for the first side (Imaging Path " + (firstSideA ? "A" : "B") + ") on the Devices Panel"); } return false; } if (getNumSides() > 1 && secondCamera == null) { if (showWarnings) { MyDialogUtils.showError("Please select a valid camera for the second side (Imaging Path " + (firstSideA ? "B" : "A") + ") on the Devices Panel."); } return false; } return true; } /** * used for updateAcquisitionStatus() calls */ private static enum AcquisitionStatus { NONE, ACQUIRING, WAITING, DONE, } private void updateAcquisitionStatus(AcquisitionStatus phase) { updateAcquisitionStatus(phase, 0); } private void updateAcquisitionStatus(AcquisitionStatus phase, int secsToNextAcquisition) { String text = ""; switch (phase) { case NONE: text = "No acquisition in progress."; break; case ACQUIRING: text = "Acquiring time point " + NumberUtils.intToDisplayString(numTimePointsDone_) + " of " + NumberUtils.intToDisplayString(getNumTimepoints()); // TODO make sure the number of timepoints can't change during an acquisition // (or maybe we make a hidden feature where the acquisition can be terminated by changing) break; case WAITING: text = "Next timepoint (" + NumberUtils.intToDisplayString(numTimePointsDone_ + 1) + " of " + NumberUtils.intToDisplayString(getNumTimepoints()) + ") in " + NumberUtils.intToDisplayString(secsToNextAcquisition) + " s."; break; case DONE: text = "Acquisition finished with " + NumberUtils.intToDisplayString(numTimePointsDone_) + " time points."; break; default: break; } acquisitionStatusLabel_.setText(text); } private boolean requiresPiezos(AcquisitionModes.Keys mode) { switch (mode) { case STAGE_SCAN: case NONE: case SLICE_SCAN_ONLY: case STAGE_SCAN_INTERLEAVED: case STAGE_SCAN_UNIDIRECTIONAL: case NO_SCAN: return false; case PIEZO_SCAN_ONLY: case PIEZO_SLICE_SCAN: return true; default: MyDialogUtils.showError("Unspecified acquisition mode " + mode.toString()); return true; } } /** * runs a test acquisition with the following features: * - not saved to disk * - window can be closed without prompting to save * - timepoints disabled * - autofocus disabled * @param side Devices.Sides.NONE to run as specified in acquisition tab, * Devices.Side.A or B to run only that side */ public void runTestAcquisition(final Devices.Sides side) { Runnable runTestThread = new Runnable() { @Override public void run() { ReportingUtils.logDebugMessage("User requested start of test diSPIM acquisition with side " + side.toString() + " selected."); cancelAcquisition_.set(false); acquisitionRequested_.set(true); updateStartButton(); boolean success = runAcquisitionPrivate(true, side); if (!success) { ReportingUtils.logError("Fatal error running test diSPIM acquisition."); } acquisitionRequested_.set(false); acquisitionRunning_.set(false); updateStartButton(); // deskew automatically if we were supposed to AcquisitionModes.Keys spimMode = getAcquisitionMode(); if (spimMode == AcquisitionModes.Keys.STAGE_SCAN || spimMode == AcquisitionModes.Keys.STAGE_SCAN_INTERLEAVED || spimMode == AcquisitionModes.Keys.STAGE_SCAN_UNIDIRECTIONAL) { if (prefs_.getBoolean(MyStrings.PanelNames.DATAANALYSIS.toString(), Properties.Keys.PLUGIN_DESKEW_AUTO_TEST, false)) { ASIdiSPIM.getFrame().getDataAnalysisPanel().runDeskew(acquisitionPanel_); } } } }; (new Thread(runTestThread, "Run Test")).start(); } /** * Implementation of acquisition that orchestrates image * acquisition itself rather than using the acquisition engine. * * This methods is public so that the ScriptInterface can call it * Please do not access this yourself directly, instead use the API, e.g. * import org.micromanager.asidispim.api.*; * ASIdiSPIMInterface diSPIM = new ASIdiSPIMImplementation(); * diSPIM.runAcquisition(); */ public void runAcquisition() { class acqThread extends Thread { acqThread(String threadName) { super(threadName); } @Override public void run() { ReportingUtils.logDebugMessage("User requested start of diSPIM acquisition."); if (isAcquisitionRequested()) { // don't allow acquisition to be requested again, just return ReportingUtils.logError("another acquisition already running"); return; } cancelAcquisition_.set(false); acquisitionRequested_.set(true); ASIdiSPIM.getFrame().tabsSetEnabled(false); updateStartButton(); boolean success = runAcquisitionPrivate(false, Devices.Sides.NONE); if (!success) { ReportingUtils.logError("Fatal error running diSPIM acquisition."); } acquisitionRequested_.set(false); updateStartButton(); ASIdiSPIM.getFrame().tabsSetEnabled(true); } } acqThread acqt = new acqThread("diSPIM Acquisition"); acqt.start(); } private Color getChannelColor(int channelIndex) { return (colors[channelIndex % colors.length]); } /** * Actually runs the acquisition; does the dirty work of setting * up the controller, the circular buffer, starting the cameras, * grabbing the images and putting them into the acquisition, etc. * @param testAcq true if running test acquisition only (see runTestAcquisition() javadoc) * @param testAcqSide only applies to test acquisition, passthrough from runTestAcquisition() * @return true if ran without any fatal errors. */ private boolean runAcquisitionPrivate(boolean testAcq, Devices.Sides testAcqSide) { // sanity check, shouldn't call this unless we aren't running an acquisition if (gui_.isAcquisitionRunning()) { MyDialogUtils.showError("An acquisition is already running"); return false; } if (ASIdiSPIM.getFrame().getHardwareInUse()) { MyDialogUtils.showError("Hardware is being used by something else (maybe autofocus?)"); return false; } boolean liveModeOriginally = gui_.isLiveModeOn(); if (liveModeOriginally) { gui_.enableLiveMode(false); } // make sure slice timings are up to date // do this automatically; we used to prompt user if they were out of date // do this before getting snapshot of sliceTiming_ in acqSettings recalculateSliceTiming(!minSlicePeriodCB_.isSelected()); if (!sliceTiming_.valid) { MyDialogUtils.showError("Error in calculating the slice timing; is the camera mode set correctly?"); return false; } AcquisitionSettings acqSettingsOrig = getCurrentAcquisitionSettings(); if (acqSettingsOrig.cameraMode == CameraModes.Keys.LIGHT_SHEET && core_.getPixelSizeUm() < 1e-6) { // can't compare equality directly with floating point values so call < 1e-9 is zero or negative ReportingUtils.showError("Need to configure pixel size in Micro-Manager to use light sheet mode."); return false; } // if a test acquisition then only run single timpoint, no autofocus // allow multi-positions for test acquisition for now, though perhaps this is not desirable if (testAcq) { acqSettingsOrig.useTimepoints = false; acqSettingsOrig.numTimepoints = 1; acqSettingsOrig.useAutofocus = false; acqSettingsOrig.separateTimepoints = false; // if called from the setup panels then the side will be specified // so we can do an appropriate single-sided acquisition // if called from the acquisition panel then NONE will be specified // and run according to existing settings if (testAcqSide != Devices.Sides.NONE) { acqSettingsOrig.numSides = 1; acqSettingsOrig.firstSideIsA = (testAcqSide == Devices.Sides.A); } // work around limitation of not being able to use PLogic per-volume switching with single side // => do per-volume switching instead (only difference should be extra time to switch) if (acqSettingsOrig.useChannels && acqSettingsOrig.channelMode == MultichannelModes.Keys.VOLUME_HW && acqSettingsOrig.numSides < 2) { acqSettingsOrig.channelMode = MultichannelModes.Keys.VOLUME; } } double volumeDuration = computeActualVolumeDuration(acqSettingsOrig); double timepointDuration = computeTimepointDuration(); long timepointIntervalMs = Math.round(acqSettingsOrig.timepointInterval * 1000); // use hardware timing if < 1 second between timepoints // experimentally need ~0.5 sec to set up acquisition, this gives a bit of cushion // cannot do this in getCurrentAcquisitionSettings because of mutually recursive // call with computeActualVolumeDuration() if (acqSettingsOrig.numTimepoints > 1 && timepointIntervalMs < (timepointDuration + 750) && !acqSettingsOrig.isStageScanning) { acqSettingsOrig.hardwareTimepoints = true; } if (acqSettingsOrig.useMultiPositions) { if (acqSettingsOrig.hardwareTimepoints || ((acqSettingsOrig.numTimepoints > 1) && (timepointIntervalMs < timepointDuration * 1.2))) { // change to not hardwareTimepoints and warn user // but allow acquisition to continue acqSettingsOrig.hardwareTimepoints = false; MyDialogUtils.showError("Timepoint interval may not be sufficient " + "depending on actual time required to change positions. " + "Proceed at your own risk."); } } // now acqSettings should be read-only final AcquisitionSettings acqSettings = acqSettingsOrig; // generate string for log file Gson gson = new GsonBuilder().setPrettyPrinting().create(); final String acqSettingsJSON = gson.toJson(acqSettings); // get MM device names for first/second cameras to acquire String firstCamera, secondCamera; Devices.Keys firstCameraKey, secondCameraKey; boolean firstSideA = acqSettings.firstSideIsA; if (firstSideA) { firstCamera = devices_.getMMDevice(Devices.Keys.CAMERAA); firstCameraKey = Devices.Keys.CAMERAA; secondCamera = devices_.getMMDevice(Devices.Keys.CAMERAB); secondCameraKey = Devices.Keys.CAMERAB; } else { firstCamera = devices_.getMMDevice(Devices.Keys.CAMERAB); firstCameraKey = Devices.Keys.CAMERAB; secondCamera = devices_.getMMDevice(Devices.Keys.CAMERAA); secondCameraKey = Devices.Keys.CAMERAA; } boolean sideActiveA, sideActiveB; final boolean twoSided = acqSettings.numSides > 1; if (twoSided) { sideActiveA = true; sideActiveB = true; } else { secondCamera = null; if (firstSideA) { sideActiveA = true; sideActiveB = false; } else { sideActiveA = false; sideActiveB = true; } } final boolean acqBothCameras = acqSettings.acquireBothCamerasSimultaneously; boolean camActiveA = sideActiveA || acqBothCameras; boolean camActiveB = sideActiveB || acqBothCameras; if (camActiveA) { if (!devices_.isValidMMDevice(Devices.Keys.CAMERAA)) { MyDialogUtils.showError("Using side A but no camera specified for that side."); return false; } Devices.Keys camKey = Devices.Keys.CAMERAA; Devices.Libraries camLib = devices_.getMMDeviceLibrary(camKey); if (!CameraModes.getValidModeKeys(camLib).contains(getSPIMCameraMode())) { MyDialogUtils.showError("Camera trigger mode set to " + getSPIMCameraMode().toString() + " but camera A doesn't support it."); return false; } // Hamamatsu only supports light sheet mode with USB cameras. Tt seems due to static architecture of getValidModeKeys // there is no good way to tell earlier that light sheet mode isn't supported. I don't like this but don't see another option. if (camLib == Devices.Libraries.HAMCAM && props_.getPropValueString(camKey, Properties.Keys.CAMERA_BUS) .equals(Properties.Values.USB3)) { if (getSPIMCameraMode() == CameraModes.Keys.LIGHT_SHEET) { MyDialogUtils.showError("Hamamatsu only supports light sheet mode with CameraLink readout."); return false; } } } if (sideActiveA) { if (!devices_.isValidMMDevice(Devices.Keys.GALVOA)) { MyDialogUtils.showError("Using side A but no scanner specified for that side."); return false; } if (requiresPiezos(acqSettings.spimMode) && !devices_.isValidMMDevice(Devices.Keys.PIEZOA)) { MyDialogUtils.showError( "Using side A and acquisition mode requires piezos but no piezo specified for that side."); return false; } } if (camActiveB) { if (!devices_.isValidMMDevice(Devices.Keys.CAMERAB)) { MyDialogUtils.showError("Using side B but no camera specified for that side."); return false; } if (!CameraModes.getValidModeKeys(devices_.getMMDeviceLibrary(Devices.Keys.CAMERAB)) .contains(getSPIMCameraMode())) { MyDialogUtils.showError("Camera trigger mode set to " + getSPIMCameraMode().toString() + " but camera B doesn't support it."); return false; } } if (sideActiveB) { if (!devices_.isValidMMDevice(Devices.Keys.GALVOB)) { MyDialogUtils.showError("Using side B but no scanner specified for that side."); return false; } if (requiresPiezos(acqSettings.spimMode) && !devices_.isValidMMDevice(Devices.Keys.PIEZOB)) { MyDialogUtils.showError( "Using side B and acquisition mode requires piezos but no piezo specified for that side."); return false; } } boolean usingDemoCam = (devices_.getMMDeviceLibrary(Devices.Keys.CAMERAA).equals(Devices.Libraries.DEMOCAM) && camActiveA) || (devices_.getMMDeviceLibrary(Devices.Keys.CAMERAB).equals(Devices.Libraries.DEMOCAM) && camActiveB); // set up channels int nrChannelsSoftware = acqSettings.numChannels; // how many times we trigger the controller per stack int nrSlicesSoftware = acqSettings.numSlices; String originalChannelConfig = ""; boolean changeChannelPerVolumeSoftware = false; if (acqSettings.useChannels) { if (acqSettings.numChannels < 1) { MyDialogUtils.showError("\"Channels\" is checked, but no channels are selected"); return false; } // get current channel so that we can restore it, then set channel appropriately originalChannelConfig = multiChannelPanel_.getCurrentConfig(); switch (acqSettings.channelMode) { case VOLUME: changeChannelPerVolumeSoftware = true; multiChannelPanel_.initializeChannelCycle(); break; case VOLUME_HW: case SLICE_HW: if (acqSettings.numChannels == 1) { // only 1 channel selected so don't have to really use hardware switching multiChannelPanel_.initializeChannelCycle(); multiChannelPanel_.selectNextChannel(); } else { // we have at least 2 channels boolean success = controller_.setupHardwareChannelSwitching(acqSettings); if (!success) { MyDialogUtils.showError("Couldn't set up slice hardware channel switching."); return false; } nrChannelsSoftware = 1; nrSlicesSoftware = acqSettings.numSlices * acqSettings.numChannels; } break; default: MyDialogUtils .showError("Unsupported multichannel mode \"" + acqSettings.channelMode.toString() + "\""); return false; } } if (twoSided && acqBothCameras) { nrSlicesSoftware *= 2; } if (acqSettings.hardwareTimepoints) { // in hardwareTimepoints case we trigger controller once for all timepoints => need to // adjust number of frames we expect back from the camera during MM's SequenceAcquisition if (acqSettings.cameraMode == CameraModes.Keys.OVERLAP) { // For overlap mode we are send one extra trigger per channel per side for volume-switching (both PLogic and not) // This holds for all multi-channel modes, just the order in which the extra trigger comes varies // Very last trigger won't ever return a frame so subtract 1. nrSlicesSoftware = ((acqSettings.numSlices + 1) * acqSettings.numChannels * acqSettings.numTimepoints); if (twoSided && acqBothCameras) { nrSlicesSoftware *= 2; } nrSlicesSoftware -= 1; } else { // we get back one image per trigger for all trigger modes other than OVERLAP // and we have already computed how many images that is (nrSlicesSoftware) nrSlicesSoftware *= acqSettings.numTimepoints; if (twoSided && acqBothCameras) { nrSlicesSoftware *= 2; } } } // set up XY positions int nrPositions = 1; PositionList positionList = new PositionList(); if (acqSettings.useMultiPositions) { try { positionList = gui_.getPositionList(); nrPositions = positionList.getNumberOfPositions(); } catch (MMScriptException ex) { MyDialogUtils.showError(ex, "Error getting position list for multiple XY positions"); } if (nrPositions < 1) { MyDialogUtils.showError("\"Positions\" is checked, but no positions are in position list"); return false; } } // make sure we have cameras selected if (!checkCamerasAssigned(true)) { return false; } final float cameraReadoutTime = computeCameraReadoutTime(); final double exposureTime = acqSettings.sliceTiming.cameraExposure; final boolean save = saveCB_.isSelected() && !testAcq; final String rootDir = rootField_.getText(); // make sure we have a valid directory to save in final File dir = new File(rootDir); if (save) { try { if (!dir.exists()) { if (!dir.mkdir()) { throw new Exception(); } } } catch (Exception ex) { MyDialogUtils.showError("Could not create directory for saving acquisition data."); return false; } } if (acqSettings.separateTimepoints) { // because separate timepoints closes windows when done, force the user to save data to disk to avoid confusion if (!save) { MyDialogUtils.showError("For separate timepoints, \"Save while acquiring\" must be enabled."); return false; } // for separate timepoints, make sure the directory is empty to make sure naming pattern is "clean" // this is an arbitrary choice to avoid confusion later on when looking at file names if (dir.list().length > 0) { MyDialogUtils.showError("For separate timepoints the saving directory must be empty."); return false; } } int nrFrames; // how many Micro-manager "frames" = time points to take if (acqSettings.separateTimepoints) { nrFrames = 1; nrRepeats_ = acqSettings.numTimepoints; } else { nrFrames = acqSettings.numTimepoints; nrRepeats_ = 1; } AcquisitionModes.Keys spimMode = acqSettings.spimMode; boolean autoShutter = core_.getAutoShutter(); boolean shutterOpen = false; // will read later String originalCamera = core_.getCameraDevice(); // more sanity checks // TODO move these checks earlier, before we set up channels and XY positions // make sure stage scan is supported if selected if (acqSettings.isStageScanning) { if (!devices_.isTigerDevice(Devices.Keys.XYSTAGE) || !props_.hasProperty(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_NUMLINES)) { MyDialogUtils.showError("Must have stage with scan-enabled firmware for stage scanning."); return false; } if (acqSettings.spimMode == AcquisitionModes.Keys.STAGE_SCAN_INTERLEAVED && acqSettings.numSides < 2) { MyDialogUtils.showError("Interleaved mode requires two sides."); return false; } } double sliceDuration = acqSettings.sliceTiming.sliceDuration; if (exposureTime + cameraReadoutTime > sliceDuration) { // should only only possible to mess this up using advanced timing settings // or if there are errors in our own calculations MyDialogUtils.showError("Exposure time of " + exposureTime + " is longer than time needed for a line scan with" + " readout time of " + cameraReadoutTime + "\n" + "This will result in dropped frames. " + "Please change input"); return false; } // if we want to do hardware timepoints make sure there's not a problem // lots of different situations where hardware timepoints can't be used... if (acqSettings.hardwareTimepoints) { if (acqSettings.useChannels && acqSettings.channelMode == MultichannelModes.Keys.VOLUME_HW) { // both hardware time points and volume channel switching use SPIMNumRepeats property // TODO this seems a severe limitation, maybe this could be changed in the future via firmware change MyDialogUtils.showError("Cannot use hardware time points (small time point interval)" + " with hardware channel switching volume-by-volume."); return false; } if (acqSettings.isStageScanning) { // stage scanning needs to be triggered for each time point MyDialogUtils.showError( "Cannot use hardware time points (small time point interval)" + " with stage scanning."); return false; } if (acqSettings.separateTimepoints) { MyDialogUtils.showError("Cannot use hardware time points (small time point interval)" + " with separate viewers/file for each time point."); return false; } if (acqSettings.useAutofocus) { MyDialogUtils.showError("Cannot use hardware time points (small time point interval)" + " with autofocus during acquisition."); return false; } if (acqSettings.useMovementCorrection) { MyDialogUtils.showError("Cannot use hardware time points (small time point interval)" + " with movement correction during acquisition."); return false; } if (acqSettings.useChannels && acqSettings.channelMode == MultichannelModes.Keys.VOLUME) { MyDialogUtils.showError("Cannot use hardware time points (small time point interval)" + " with software channels (need to use PLogic channel switching)."); return false; } if (spimMode == AcquisitionModes.Keys.NO_SCAN) { MyDialogUtils.showError("Cannot do hardware time points when no scan mode is used." + " Use the number of slices to set the number of images to acquire."); return false; } } if (acqSettings.useChannels && acqSettings.channelMode == MultichannelModes.Keys.VOLUME_HW && acqSettings.numSides < 2) { MyDialogUtils.showError("Cannot do PLogic channel switching of volume when only one" + " side is selected. Pester the developers if you need this."); return false; } // make sure we aren't trying to collect timepoints faster than we can if (!acqSettings.useMultiPositions && acqSettings.numTimepoints > 1) { if (timepointIntervalMs < volumeDuration) { MyDialogUtils .showError("Time point interval shorter than" + " the time to collect a single volume.\n"); return false; } } // Autofocus settings; only used if acqSettings.useAutofocus is true boolean autofocusAtT0 = false; int autofocusEachNFrames = 10; String autofocusChannel = ""; if (acqSettings.useAutofocus) { autofocusAtT0 = prefs_.getBoolean(MyStrings.PanelNames.AUTOFOCUS.toString(), Properties.Keys.PLUGIN_AUTOFOCUS_ACQBEFORESTART, false); autofocusEachNFrames = props_.getPropValueInteger(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_AUTOFOCUS_EACHNIMAGES); autofocusChannel = props_.getPropValueString(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_AUTOFOCUS_CHANNEL); // double-check that selected channel is valid if we are doing multi-channel if (acqSettings.useChannels) { String channelGroup = props_.getPropValueString(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_MULTICHANNEL_GROUP); StrVector channels = gui_.getMMCore().getAvailableConfigs(channelGroup); boolean found = false; for (String channel : channels) { if (channel.equals(autofocusChannel)) { found = true; break; } } if (!found) { MyDialogUtils.showError("Invalid autofocus channel selected on autofocus tab."); return false; } } } // Movement Correction settings; only used if acqSettings.useMovementCorrection is true int correctMovementEachNFrames = 10; String correctMovementChannel = ""; int cmChannelNumber = -1; if (acqSettings.useMovementCorrection) { correctMovementEachNFrames = props_.getPropValueInteger(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_AUTOFOCUS_CORRECTMOVEMENT_EACHNIMAGES); correctMovementChannel = props_.getPropValueString(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_AUTOFOCUS_CORRECTMOVEMENT_CHANNEL); // double-check that selected channel is valid if we are doing multi-channel if (acqSettings.useChannels) { String channelGroup = props_.getPropValueString(Devices.Keys.PLUGIN, Properties.Keys.PLUGIN_MULTICHANNEL_GROUP); StrVector channels = gui_.getMMCore().getAvailableConfigs(channelGroup); boolean found = false; for (String channel : channels) { if (channel.equals(correctMovementChannel)) { found = true; break; } } if (!found) { MyDialogUtils.showError("Invalid movement correction channel selected on autofocus tab."); return false; } } } // the circular buffer, which is used by both cameras, can only have one image size setting // => require same image height and width for both cameras if both are used if (twoSided || acqBothCameras) { try { Rectangle roi_1 = core_.getROI(firstCamera); Rectangle roi_2 = core_.getROI(secondCamera); if (roi_1.width != roi_2.width || roi_1.height != roi_2.height) { MyDialogUtils.showError( "Two cameras' ROI height and width must be equal because of Micro-Manager's circular buffer"); return false; } } catch (Exception ex) { MyDialogUtils.showError(ex, "Problem getting camera ROIs"); } } cameras_.setCameraForAcquisition(firstCameraKey, true); if (twoSided || acqBothCameras) { cameras_.setCameraForAcquisition(secondCameraKey, true); } // save exposure time, will restore at end of acquisition try { prefs_.putFloat(MyStrings.PanelNames.SETTINGS.toString(), Properties.Keys.PLUGIN_CAMERA_LIVE_EXPOSURE_FIRST.toString(), (float) core_.getExposure(devices_.getMMDevice(firstCameraKey))); if (twoSided || acqBothCameras) { prefs_.putFloat(MyStrings.PanelNames.SETTINGS.toString(), Properties.Keys.PLUGIN_CAMERA_LIVE_EXPOSURE_SECOND.toString(), (float) core_.getExposure(devices_.getMMDevice(secondCameraKey))); } } catch (Exception ex) { MyDialogUtils.showError(ex, "could not cache exposure"); } try { core_.setExposure(firstCamera, exposureTime); if (twoSided || acqBothCameras) { core_.setExposure(secondCamera, exposureTime); } gui_.refreshGUIFromCache(); } catch (Exception ex) { MyDialogUtils.showError(ex, "could not set exposure"); } // seems to have a problem if the core's camera has been set to some other // camera before we start doing things, so set to a SPIM camera try { core_.setCameraDevice(firstCamera); } catch (Exception ex) { MyDialogUtils.showError(ex, "could not set camera"); } // empty out circular buffer try { core_.clearCircularBuffer(); } catch (Exception ex) { MyDialogUtils.showError(ex, "Error emptying out the circular buffer"); return false; } // stop the serial traffic for position updates during acquisition // if we return from this function (including aborting) we need to unpause posUpdater_.pauseUpdates(true); // initialize stage scanning so we can restore state Point2D.Double xyPosUm = new Point2D.Double(); float origXSpeed = 1f; // don't want 0 in case something goes wrong float origXAccel = 1f; // don't want 0 in case something goes wrong if (acqSettings.isStageScanning) { try { xyPosUm = core_.getXYStagePosition(devices_.getMMDevice(Devices.Keys.XYSTAGE)); origXSpeed = props_.getPropValueFloat(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_MOTOR_SPEED); origXAccel = props_.getPropValueFloat(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_MOTOR_ACCEL); } catch (Exception ex) { MyDialogUtils.showError( "Could not get XY stage position, speed, or acceleration for stage scan initialization"); posUpdater_.pauseUpdates(false); return false; } // if X speed is less than 0.2 mm/s then it probably wasn't restored to correct speed some other time // we offer to set it to a more normal speed in that case, until the user declines and we stop asking if (origXSpeed < 0.2 && resetXaxisSpeed_) { resetXaxisSpeed_ = MyDialogUtils.getConfirmDialogResult( "Max speed of X axis is small, perhaps it was not correctly restored after stage scanning previously. Do you want to set it to 1 mm/s now?", JOptionPane.YES_NO_OPTION); // once the user selects "no" then resetXaxisSpeed_ will be false and stay false until plugin is launched again if (resetXaxisSpeed_) { props_.setPropValue(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_MOTOR_SPEED, 1f); origXSpeed = 1f; } } } numTimePointsDone_ = 0; // force saving as image stacks, not individual files // implementation assumes just two options, either // TaggedImageStorageDiskDefault.class or TaggedImageStorageMultipageTiff.class boolean separateImageFilesOriginally = ImageUtils.getImageStorageClass() .equals(TaggedImageStorageDiskDefault.class); ImageUtils.setImageStorageClass(TaggedImageStorageMultipageTiff.class); // Set up controller SPIM parameters (including from Setup panel settings) // want to do this, even with demo cameras, so we can test everything else if (!controller_.prepareControllerForAquisition(acqSettings)) { posUpdater_.pauseUpdates(false); return false; } boolean nonfatalError = false; long acqButtonStart = System.currentTimeMillis(); String acqName = ""; acq_ = null; // do not want to return from within this loop => throw exception instead // loop is executed once per acquisition (i.e. once if separate viewers isn't selected // or once per timepoint if separate viewers is selected) long repeatStart = System.currentTimeMillis(); for (int acqNum = 0; !cancelAcquisition_.get() && acqNum < nrRepeats_; acqNum++) { // handle intervals between (software-timed) repeats // only applies when doing separate viewers for each timepoint // and have multiple timepoints long repeatNow = System.currentTimeMillis(); long repeatdelay = repeatStart + acqNum * timepointIntervalMs - repeatNow; while (repeatdelay > 0 && !cancelAcquisition_.get()) { updateAcquisitionStatus(AcquisitionStatus.WAITING, (int) (repeatdelay / 1000)); long sleepTime = Math.min(1000, repeatdelay); try { Thread.sleep(sleepTime); } catch (InterruptedException e) { ReportingUtils.showError(e); } repeatNow = System.currentTimeMillis(); repeatdelay = repeatStart + acqNum * timepointIntervalMs - repeatNow; } BlockingQueue<TaggedImage> bq = new LinkedBlockingQueue<TaggedImage>(10); // try to close last acquisition viewer if there could be one open (only in single acquisition per timepoint mode) if (acqSettings.separateTimepoints && (acq_ != null) && !cancelAcquisition_.get()) { try { // following line needed due to some arcane internal reason, otherwise // call to closeAcquisitionWindow() fails silently. // See http://sourceforge.net/p/micro-manager/mailman/message/32999320/ acq_.promptToSave(false); gui_.closeAcquisitionWindow(acqName); } catch (Exception ex) { // do nothing if unsuccessful } } if (acqSettings.separateTimepoints) { // call to getUniqueAcquisitionName is extra safety net, we have checked that directory is empty before starting acqName = gui_.getUniqueAcquisitionName(prefixField_.getText() + "_" + acqNum); } else { acqName = gui_.getUniqueAcquisitionName(prefixField_.getText()); } long extraStageScanTimeout = 0; if (acqSettings.isStageScanning) { // approximately compute the extra time to wait for stack to begin (ramp up time) // by getting the volume duration and subtracting the acquisition duration and then dividing by two extraStageScanTimeout = (long) Math.ceil(computeActualVolumeDuration(acqSettings) - (acqSettings.numSlices * acqSettings.numChannels * acqSettings.sliceTiming.sliceDuration)) / 2; } long extraMultiXYTimeout = 0; if (acqSettings.useMultiPositions) { // give 20 extra seconds to arrive at intended XY position instead of trying to get fancy about computing actual move time extraMultiXYTimeout = XYSTAGETIMEOUT; // furthermore make sure that the main timeout value is at least 20ms because MM's position list uses this (via MultiStagePosition.goToPosition) if (props_.getPropValueInteger(Devices.Keys.CORE, Properties.Keys.CORE_TIMEOUT_MS) < XYSTAGETIMEOUT) { props_.setPropValue(Devices.Keys.CORE, Properties.Keys.CORE_TIMEOUT_MS, XYSTAGETIMEOUT); } } VirtualAcquisitionDisplay vad = null; WindowListener wl_acq = null; WindowListener[] wls_orig = null; try { // check for stop button before each acquisition if (cancelAcquisition_.get()) { throw new IllegalMonitorStateException("User stopped the acquisition"); } // flag that we are actually running acquisition now acquisitionRunning_.set(true); ReportingUtils.logMessage("diSPIM plugin starting acquisition " + acqName + " with following settings: " + acqSettingsJSON); final int numMMChannels = acqSettings.numSides * acqSettings.numChannels * (acqBothCameras ? 2 : 1); if (spimMode == AcquisitionModes.Keys.NO_SCAN && !acqSettings.separateTimepoints) { // swap nrFrames and numSlices gui_.openAcquisition(acqName, rootDir, acqSettings.numSlices, numMMChannels, nrFrames, nrPositions, true, save); } else { gui_.openAcquisition(acqName, rootDir, nrFrames, numMMChannels, acqSettings.numSlices, nrPositions, true, save); } channelNames_ = new String[numMMChannels]; // generate channel names and colors // also builds viewString for MultiViewRegistration metadata String viewString = ""; final String SEPARATOR = "_"; for (int reflect = 0; reflect < 2; reflect++) { // only run for loop once unless acqBothCameras is true // if acqBothCameras is true then run second time to add "epi" channels if (reflect > 0 && !acqBothCameras) { continue; } // set up channels (side A/B is treated as channel too) if (acqSettings.useChannels) { ChannelSpec[] channels = multiChannelPanel_.getUsedChannels(); for (int i = 0; i < channels.length; i++) { String chName = "-" + channels[i].config_ + (reflect > 0 ? "-epi" : ""); // same algorithm for channel index vs. specified channel and side as in comments of code below // that figures out the channel where to file each incoming image int channelIndex = i; if (twoSided) { channelIndex *= 2; } channelIndex += reflect * numMMChannels / 2; channelNames_[channelIndex] = firstCamera + chName; viewString += NumberUtils.intToDisplayString(0) + SEPARATOR; if (twoSided) { channelNames_[channelIndex + 1] = secondCamera + chName; viewString += NumberUtils.intToDisplayString(90) + SEPARATOR; } } } else { // single-channel int channelIndex = reflect * numMMChannels / 2; channelNames_[channelIndex] = firstCamera + (reflect > 0 ? "-epi" : ""); viewString += NumberUtils.intToDisplayString(0) + SEPARATOR; if (twoSided) { channelNames_[channelIndex + 1] = secondCamera + (reflect > 0 ? "-epi" : ""); viewString += NumberUtils.intToDisplayString(90) + SEPARATOR; } } } // strip last separator of viewString (for Multiview Reconstruction) viewString = viewString.substring(0, viewString.length() - 1); // assign channel names and colors for (int i = 0; i < numMMChannels; i++) { gui_.setChannelName(acqName, i, channelNames_[i]); gui_.setChannelColor(acqName, i, getChannelColor(i)); } if (acqSettings.useMovementCorrection) { for (int i = 0; i < acqSettings.numChannels; i++) { if (channelNames_[i].equals(firstCamera + "-" + correctMovementChannel)) { cmChannelNumber = i; } } if (cmChannelNumber == -1) { MyDialogUtils.showError( "The channel selected for movement correction on the auitofocus tab was not found in this acquisition"); return false; } } zStepUm_ = acqSettings.isStageScanning ? controller_.getActualStepSizeUm() // computed step size, accounting for quantization of controller : acqSettings.stepSizeUm; // should be same as PanelUtils.getSpinnerFloatValue(stepSize_) // initialize acquisition gui_.initializeAcquisition(acqName, (int) core_.getImageWidth(), (int) core_.getImageHeight(), (int) core_.getBytesPerPixel(), (int) core_.getImageBitDepth()); gui_.promptToSaveAcquisition(acqName, !testAcq); // These metadata have to be added after initialization, // otherwise they will not be shown?! gui_.setAcquisitionProperty(acqName, "NumberOfSides", NumberUtils.doubleToDisplayString(acqSettings.numSides)); gui_.setAcquisitionProperty(acqName, "FirstSide", acqSettings.firstSideIsA ? "A" : "B"); gui_.setAcquisitionProperty(acqName, "SlicePeriod_ms", actualSlicePeriodLabel_.getText()); gui_.setAcquisitionProperty(acqName, "LaserExposure_ms", NumberUtils.doubleToDisplayString(acqSettings.desiredLightExposure)); gui_.setAcquisitionProperty(acqName, "VolumeDuration", actualVolumeDurationLabel_.getText()); gui_.setAcquisitionProperty(acqName, "SPIMmode", spimMode.toString()); // Multi-page TIFF saving code wants this one (cameras are all 16-bits, so not much reason for anything else) gui_.setAcquisitionProperty(acqName, "PixelType", "GRAY16"); gui_.setAcquisitionProperty(acqName, "UseAutofocus", acqSettings.useAutofocus ? Boolean.TRUE.toString() : Boolean.FALSE.toString()); gui_.setAcquisitionProperty(acqName, "UseMotionCorrection", acqSettings.useMovementCorrection ? Boolean.TRUE.toString() : Boolean.FALSE.toString()); gui_.setAcquisitionProperty(acqName, "HardwareTimepoints", acqSettings.hardwareTimepoints ? Boolean.TRUE.toString() : Boolean.FALSE.toString()); gui_.setAcquisitionProperty(acqName, "SeparateTimepoints", acqSettings.separateTimepoints ? Boolean.TRUE.toString() : Boolean.FALSE.toString()); gui_.setAcquisitionProperty(acqName, "CameraMode", acqSettings.cameraMode.toString()); gui_.setAcquisitionProperty(acqName, "z-step_um", NumberUtils.doubleToDisplayString(zStepUm_)); // Properties for use by MultiViewRegistration plugin // Format is: x_y_z, set to 1 if we should rotate around this axis. gui_.setAcquisitionProperty(acqName, "MVRotationAxis", "0_1_0"); gui_.setAcquisitionProperty(acqName, "MVRotations", viewString); // save XY and SPIM head position in metadata // update positions first at expense of two extra serial transactions refreshXYZPositions(); gui_.setAcquisitionProperty(acqName, "Position_X", positions_.getPositionString(Devices.Keys.XYSTAGE, Directions.X)); gui_.setAcquisitionProperty(acqName, "Position_Y", positions_.getPositionString(Devices.Keys.XYSTAGE, Directions.Y)); gui_.setAcquisitionProperty(acqName, "Position_SPIM_Head", positions_.getPositionString(Devices.Keys.UPPERZDRIVE)); gui_.setAcquisitionProperty(acqName, "SPIMAcqSettings", acqSettingsJSON); gui_.setAcquisitionProperty(acqName, "SPIMtype", ASIdiSPIM.oSPIM ? "oSPIM" : "diSPIM"); gui_.setAcquisitionProperty(acqName, "AcquisitionName", acqName); gui_.setAcquisitionProperty(acqName, "Prefix", acqName); // get circular buffer ready // do once here but not per-trigger; need to ensure ROI changes registered core_.initializeCircularBuffer(); // superset of clearCircularBuffer() // TODO: use new acquisition interface that goes through the pipeline //gui_.setAcquisitionAddImageAsynchronous(acqName); acq_ = gui_.getAcquisition(acqName); // Dive into MM internals since script interface does not support pipelines ImageCache imageCache = acq_.getImageCache(); vad = acq_.getAcquisitionWindow(); imageCache.addImageCacheListener(vad); // Start pumping images into the ImageCache DefaultTaggedImageSink sink = new DefaultTaggedImageSink(bq, imageCache); sink.start(); // remove usual window listener(s) and replace it with our own // that will prompt before closing and cancel acquisition if confirmed // this should be considered a hack, it may not work perfectly // I have confirmed that there is only one windowListener and it seems to // also be related to window closing // Note that ImageJ's acquisition window is AWT instead of Swing wls_orig = vad.getImagePlus().getWindow().getWindowListeners(); for (WindowListener l : wls_orig) { vad.getImagePlus().getWindow().removeWindowListener(l); } wl_acq = new WindowAdapter() { @Override public void windowClosing(WindowEvent arg0) { // if running acquisition only close if user confirms if (acquisitionRunning_.get()) { boolean stop = MyDialogUtils.getConfirmDialogResult( "Do you really want to abort the acquisition?", JOptionPane.YES_NO_OPTION); if (stop) { cancelAcquisition_.set(true); } } } }; vad.getImagePlus().getWindow().addWindowListener(wl_acq); // patterned after implementation in MMStudio.java // will be null if not saving to disk lastAcquisitionPath_ = acq_.getImageCache().getDiskLocation(); lastAcquisitionName_ = acqName; // only used when motion correction was requested MovementDetector[] movementDetectors = new MovementDetector[nrPositions]; // Transformation matrices to convert between camera and stage coordinates final Vector3D yAxis = new Vector3D(0.0, 1.0, 0.0); final Rotation camARotation = new Rotation(yAxis, Math.toRadians(-45)); final Rotation camBRotation = new Rotation(yAxis, Math.toRadians(45)); final Vector3D zeroPoint = new Vector3D(0.0, 0.0, 0.0); // cache a zero point for efficiency // make sure all devices have arrived, e.g. a stage isn't still moving try { core_.waitForSystem(); } catch (Exception e) { ReportingUtils.logError("error waiting for system"); } // Loop over all the times we trigger the controller's acquisition // (although if multi-channel with volume switching is selected there // is inner loop to trigger once per channel) // remember acquisition start time for software-timed timepoints // For hardware-timed timepoints we only trigger the controller once long acqStart = System.currentTimeMillis(); for (int trigNum = 0; trigNum < nrFrames; trigNum++) { // handle intervals between (software-timed) time points // when we are within the same acquisition // (if separate viewer is selected then nothing bad happens here // but waiting during interval handled elsewhere) long acqNow = System.currentTimeMillis(); long delay = acqStart + trigNum * timepointIntervalMs - acqNow; while (delay > 0 && !cancelAcquisition_.get()) { updateAcquisitionStatus(AcquisitionStatus.WAITING, (int) (delay / 1000)); long sleepTime = Math.min(1000, delay); Thread.sleep(sleepTime); acqNow = System.currentTimeMillis(); delay = acqStart + trigNum * timepointIntervalMs - acqNow; } // check for stop button before each time point if (cancelAcquisition_.get()) { throw new IllegalMonitorStateException("User stopped the acquisition"); } int timePoint = acqSettings.separateTimepoints ? acqNum : trigNum; // this is where we autofocus if requested if (acqSettings.useAutofocus) { // Note that we will not autofocus as expected when using hardware // timing. Seems OK, since hardware timing will result in short // acquisition times that do not need autofocus. We have already // ensured that we aren't doing both if ((autofocusAtT0 && timePoint == 0) || ((timePoint > 0) && (timePoint % autofocusEachNFrames == 0))) { if (acqSettings.useChannels) { multiChannelPanel_.selectChannel(autofocusChannel); } if (sideActiveA) { AutofocusUtils.FocusResult score = autofocus_.runFocus(this, Devices.Sides.A, false, sliceTiming_, false); updateCalibrationOffset(Devices.Sides.A, score); } if (sideActiveB) { AutofocusUtils.FocusResult score = autofocus_.runFocus(this, Devices.Sides.B, false, sliceTiming_, false); updateCalibrationOffset(Devices.Sides.B, score); } // Restore settings of the controller controller_.prepareControllerForAquisition(acqSettings); if (acqSettings.useChannels && acqSettings.channelMode != MultichannelModes.Keys.VOLUME) { controller_.setupHardwareChannelSwitching(acqSettings); } // make sure circular buffer is cleared core_.clearCircularBuffer(); } } numTimePointsDone_++; updateAcquisitionStatus(AcquisitionStatus.ACQUIRING); // loop over all positions for (int positionNum = 0; positionNum < nrPositions; positionNum++) { if (acqSettings.useMultiPositions) { // make sure user didn't stop things if (cancelAcquisition_.get()) { throw new IllegalMonitorStateException("User stopped the acquisition"); } // want to move between positions move stage fast, so we // will clobber stage scanning setting so need to restore it float scanXSpeed = 1f; float scanXAccel = 1f; if (acqSettings.isStageScanning) { scanXSpeed = props_.getPropValueFloat(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_MOTOR_SPEED); props_.setPropValue(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_MOTOR_SPEED, origXSpeed); scanXAccel = props_.getPropValueFloat(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_MOTOR_ACCEL); props_.setPropValue(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_MOTOR_ACCEL, origXAccel); } final MultiStagePosition nextPosition = positionList.getPosition(positionNum); // blocking call; will wait for stages to move MultiStagePosition.goToPosition(nextPosition, core_); // for stage scanning: restore speed and set up scan at new position // non-multi-position situation is handled in prepareControllerForAquisition instead if (acqSettings.isStageScanning) { props_.setPropValue(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_MOTOR_SPEED, scanXSpeed); props_.setPropValue(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_MOTOR_ACCEL, scanXAccel); StagePosition pos = nextPosition.get(devices_.getMMDevice(Devices.Keys.XYSTAGE)); // get ideal position from position list, not current position controller_.prepareStageScanForAcquisition(pos.x, pos.y); } refreshXYZPositions(); // wait any extra time the user requests Thread.sleep(Math.round(PanelUtils.getSpinnerFloatValue(positionDelay_))); } // loop over all the times we trigger the controller // usually just once, but will be the number of channels if we have // multiple channels and aren't using PLogic to change between them for (int channelNum = 0; channelNum < nrChannelsSoftware; channelNum++) { try { // flag that we are using the cameras/controller ASIdiSPIM.getFrame().setHardwareInUse(true); // deal with shutter before starting acquisition shutterOpen = core_.getShutterOpen(); if (autoShutter) { core_.setAutoShutter(false); if (!shutterOpen) { core_.setShutterOpen(true); } } // start the cameras core_.startSequenceAcquisition(firstCamera, nrSlicesSoftware, 0, true); if (twoSided || acqBothCameras) { core_.startSequenceAcquisition(secondCamera, nrSlicesSoftware, 0, true); } // deal with channel if needed (hardware channel switching doesn't happen here) if (changeChannelPerVolumeSoftware) { multiChannelPanel_.selectNextChannel(); } // special case: single-sided piezo acquisition risks illumination piezo sleeping // prevent this from happening by sending relative move of 0 like we do in live mode before each trigger // NB: this won't help for hardware-timed timepoints final Devices.Keys piezoIllumKey = firstSideA ? Devices.Keys.PIEZOB : Devices.Keys.PIEZOA; if (!twoSided && props_.getPropValueInteger(piezoIllumKey, Properties.Keys.AUTO_SLEEP_DELAY) > 0) { core_.setRelativePosition(devices_.getMMDevice(piezoIllumKey), 0); } // trigger the state machine on the controller // do this even with demo cameras to test everything else boolean success = controller_.triggerControllerStartAcquisition(spimMode, firstSideA); if (!success) { throw new Exception("Controller triggering not successful"); } ReportingUtils.logDebugMessage("Starting time point " + (timePoint + 1) + " of " + nrFrames + " with (software) channel number " + channelNum); // Wait for first image to create ImageWindow, so that we can be sure about image size // Do not actually grab first image here, just make sure it is there long start = System.currentTimeMillis(); long now = start; final long timeout = Math.max(3000, Math.round(10 * sliceDuration + 2 * acqSettings.delayBeforeSide)) + extraStageScanTimeout + extraMultiXYTimeout; while (core_.getRemainingImageCount() == 0 && (now - start < timeout) && !cancelAcquisition_.get()) { now = System.currentTimeMillis(); Thread.sleep(5); } if (now - start >= timeout) { String msg = "Camera did not send first image within a reasonable time.\n"; if (acqSettings.isStageScanning) { msg += "Make sure jumpers are correct on XY card and also micro-micromirror card."; } else { msg += "Make sure camera trigger cables are connected properly."; } throw new Exception(msg); } // grab all the images from the cameras, put them into the acquisition int[] channelImageNr = new int[4 * acqSettings.numChannels]; // keep track of how many frames we have received for each MM "channel" int[] cameraImageNr = new int[2]; // keep track of how many images we have received from the camera int[] tpNumber = new int[2 * acqSettings.numChannels]; // keep track of which timepoint we are on for hardware timepoints int imagesToSkip = 0; // hardware timepoints have to drop spurious images with overlap mode final boolean checkForSkips = acqSettings.hardwareTimepoints && (acqSettings.cameraMode == CameraModes.Keys.OVERLAP); boolean done = false; long timeout2 = Math.max(1000, Math.round(5 * sliceDuration)); if (acqSettings.isStageScanning) { // for stage scanning have to allow extra time for turn-around timeout2 += (2 * (long) Math.ceil(getStageRampDuration(acqSettings))); // ramp up and then down timeout2 += 5000; // ample extra time for turn-around (e.g. antibacklash move in Y), interestingly 500ms extra seems insufficient for reasons I don't understand yet so just pad this for now // TODO figure out why turn-aronud is taking so long if (acqSettings.spimMode == AcquisitionModes.Keys.STAGE_SCAN_UNIDIRECTIONAL) { timeout2 += (long) Math.ceil(getStageRetraceDuration(acqSettings)); // in unidirectional case also need to rewind } } start = System.currentTimeMillis(); long last = start; try { while ((core_.getRemainingImageCount() > 0 || core_.isSequenceRunning(firstCamera) || ((twoSided || acqBothCameras) && core_.isSequenceRunning(secondCamera))) && !done) { now = System.currentTimeMillis(); if (core_.getRemainingImageCount() > 0) { // we have an image to grab TaggedImage timg = core_.popNextTaggedImage(); if (checkForSkips && imagesToSkip != 0) { imagesToSkip--; continue; // goes to next iteration of this loop without doing anything else } // figure out which channel index this frame belongs to // "channel index" is channel of MM acquisition // channel indexes will go from 0 to (numSides * numChannels - 1) for standard (non-reflective) imaging // if double-sided then second camera gets odd channel indexes (1, 3, etc.) // and adjacent pairs will be same color (e.g. 0 and 1 will be from first color, 2 and 3 from second, etc.) // if acquisition from both cameras (reflective imaging) then // second half of channel indices are from opposite (epi) view // e.g. for 3-color 1-sided (A first) standard (non-reflective) then // 0 will be A-illum A-cam 1st color // 2 will be A-illum A-cam 2nd color // 4 will be A-illum A-cam 3rd color // e.g. for 3-color 2-sided (A first) standard (non-reflective) then // 0 will be A-illum A-cam 1st color // 1 will be B-illum B-cam 1st color // 2 will be A-illum A-cam 2nd color // 3 will be B-illum B-cam 2nd color // 4 will be A-illum A-cam 3rd color // 5 will be B-illum B-cam 3rd color // e.g. for 3-color 1-sided (A first) both camera (reflective) then // 0 will be A-illum A-cam 1st color // 1 will be A-illum A-cam 2nd color // 2 will be A-illum A-cam 3rd color // 3 will be A-illum B-cam 1st color // 4 will be A-illum B-cam 2nd color // 5 will be A-illum B-cam 3rd color // e.g. for 3-color 2-sided (A first) both camera (reflective) then // 0 will be A-illum A-cam 1st color // 1 will be B-illum B-cam 1st color // 2 will be A-illum A-cam 2nd color // 3 will be B-illum B-cam 2nd color // 4 will be A-illum A-cam 3rd color // 5 will be B-illum B-cam 3rd color // 6 will be A-illum B-cam 1st color // 7 will be B-illum A-cam 1st color // 8 will be A-illum B-cam 2nd color // 9 will be B-illum A-cam 2nd color // 10 will be A-illum B-cam 3rd color // 11 will be B-illum A-cam 3rd color String camera = (String) timg.tags.get("Camera"); int cameraIndex = camera.equals(firstCamera) ? 0 : 1; int channelIndex_tmp; switch (acqSettings.channelMode) { case NONE: case VOLUME: channelIndex_tmp = channelNum; break; case VOLUME_HW: channelIndex_tmp = cameraImageNr[cameraIndex] / acqSettings.numSlices; // want quotient only break; case SLICE_HW: channelIndex_tmp = cameraImageNr[cameraIndex] % acqSettings.numChannels; // want modulo arithmetic break; default: // should never get here throw new Exception("Undefined channel mode"); } if (acqBothCameras) { if (twoSided) { // 2-sided, both cameras channelIndex_tmp = channelIndex_tmp * 2 + cameraIndex; // determine whether first or second side by whether we've seen half the images yet if (cameraImageNr[cameraIndex] > nrSlicesSoftware / 2) { // second illumination side => second half of channels channelIndex_tmp += 2 * acqSettings.numChannels; } } else { // 1-sided, both cameras channelIndex_tmp += cameraIndex * acqSettings.numChannels; } } else { // normal situation, non-reflective imaging if (twoSided) { channelIndex_tmp *= 2; } channelIndex_tmp += cameraIndex; } final int channelIndex = channelIndex_tmp; int actualTimePoint = timePoint; if (acqSettings.hardwareTimepoints) { actualTimePoint = tpNumber[channelIndex]; } if (acqSettings.separateTimepoints) { // if we are doing separate timepoints then frame is always 0 actualTimePoint = 0; } // note that hardwareTimepoints and separateTimepoints can never both be true // add image to acquisition if (spimMode == AcquisitionModes.Keys.NO_SCAN && !acqSettings.separateTimepoints) { // create time series for no scan addImageToAcquisition(acq_, channelImageNr[channelIndex], channelIndex, actualTimePoint, positionNum, now - acqStart, timg, bq); } else { // standard, create Z-stacks addImageToAcquisition(acq_, actualTimePoint, channelIndex, channelImageNr[channelIndex], positionNum, now - acqStart, timg, bq); } // update our counters to be ready for next image channelImageNr[channelIndex]++; cameraImageNr[cameraIndex]++; // if hardware timepoints then we only send one trigger and // manually keep track of which channel/timepoint comes next if (acqSettings.hardwareTimepoints && channelImageNr[channelIndex] >= acqSettings.numSlices) { // only do this if we are done with the slices in this MM channel // we just finished filling one MM channel with all its slices so go to next timepoint for this channel channelImageNr[channelIndex] = 0; tpNumber[channelIndex]++; // see if we are supposed to skip next image if (checkForSkips) { // one extra image per MM channel, this includes case of only 1 color (either multi-channel disabled or else only 1 channel selected) // if we are interleaving by slice then next nrChannel images will be from extra slice position // any other configuration we will just drop the next image if (acqSettings.useChannels && acqSettings.channelMode == MultichannelModes.Keys.SLICE_HW) { imagesToSkip = acqSettings.numChannels; } else { imagesToSkip = 1; } } // update acquisition status message for hardware acquisition // (for non-hardware acquisition message is updated elsewhere) // Arbitrarily choose one possible channel to do this on. if (channelIndex == 0 && (numTimePointsDone_ < acqSettings.numTimepoints)) { numTimePointsDone_++; updateAcquisitionStatus(AcquisitionStatus.ACQUIRING); } } last = now; // keep track of last image timestamp } else { // no image ready yet done = cancelAcquisition_.get(); Thread.sleep(1); if (now - last >= timeout2) { ReportingUtils .logError("Camera did not send all expected images within" + " a reasonable period for timepoint " + numTimePointsDone_ + ". Continuing anyway."); nonfatalError = true; done = true; } } } // update count if we stopped in the middle if (cancelAcquisition_.get()) { numTimePointsDone_--; } // if we are using demo camera then add some extra time to let controller finish // since we got images without waiting for controller to actually send triggers if (usingDemoCam) { Thread.sleep(200); // for serial communication overhead Thread.sleep((long) volumeDuration / nrChannelsSoftware); // estimate the time per channel, not ideal in case of software channel switching if (acqSettings.isStageScanning) { Thread.sleep(1000 + extraStageScanTimeout); // extra 1 second plus ramp time for stage scanning } } } catch (InterruptedException iex) { MyDialogUtils.showError(iex); } if (acqSettings.hardwareTimepoints) { break; // only trigger controller once } } catch (Exception ex) { MyDialogUtils.showError(ex); } finally { // cleanup at the end of each time we trigger the controller ASIdiSPIM.getFrame().setHardwareInUse(false); // put shutter back to original state core_.setShutterOpen(shutterOpen); core_.setAutoShutter(autoShutter); // make sure cameras aren't running anymore if (core_.isSequenceRunning(firstCamera)) { core_.stopSequenceAcquisition(firstCamera); } if ((twoSided || acqBothCameras) && core_.isSequenceRunning(secondCamera)) { core_.stopSequenceAcquisition(secondCamera); } // make sure SPIM state machine on micromirror and SCAN of XY card are stopped (should normally be but sanity check) if ((acqSettings.numSides > 1) || acqSettings.firstSideIsA) { props_.setPropValue(Devices.Keys.GALVOA, Properties.Keys.SPIM_STATE, Properties.Values.SPIM_IDLE, true); } if ((acqSettings.numSides > 1) || !acqSettings.firstSideIsA) { props_.setPropValue(Devices.Keys.GALVOB, Properties.Keys.SPIM_STATE, Properties.Values.SPIM_IDLE, true); } if (acqSettings.isStageScanning) { props_.setPropValue(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_STATE, Properties.Values.SPIM_IDLE); } } } if (acqSettings.useMovementCorrection && (timePoint % correctMovementEachNFrames) == 0) { if (movementDetectors[positionNum] == null) { // Transform from camera space to stage space: Rotation rotation = camBRotation; if (firstSideA) { rotation = camARotation; } movementDetectors[positionNum] = new MovementDetector(prefs_, acq_, cmChannelNumber, positionNum, rotation); } Vector3D movement = movementDetectors[positionNum] .detectMovement(Method.PhaseCorrelation); String msg1 = "TimePoint: " + timePoint + ", Detected movement. X: " + movement.getX() + ", Y: " + movement.getY() + ", Z: " + movement.getZ(); System.out.println(msg1); if (!movement.equals(zeroPoint)) { String msg = "ASIdiSPIM motion corrector moving stages: X: " + movement.getX() + ", Y: " + movement.getY() + ", Z: " + movement.getZ(); gui_.logMessage(msg); System.out.println(msg); // if we are using the position list, update the position in the list if (acqSettings.useMultiPositions) { MultiStagePosition position = positionList.getPosition(positionNum); StagePosition pos = position.get(devices_.getMMDevice(Devices.Keys.XYSTAGE)); pos.x += movement.getX(); pos.y += movement.getY(); StagePosition zPos = position .get(devices_.getMMDevice(Devices.Keys.UPPERZDRIVE)); if (zPos != null) { zPos.x += movement.getZ(); } } else { // only a single position, move the stage now core_.setRelativeXYPosition(devices_.getMMDevice(Devices.Keys.XYSTAGE), movement.getX(), movement.getY()); core_.setRelativePosition(devices_.getMMDevice(Devices.Keys.UPPERZDRIVE), movement.getZ()); } } } } if (acqSettings.hardwareTimepoints) { break; } } } catch (IllegalMonitorStateException ex) { // do nothing, the acquisition was simply halted during its operation // will log error message during finally clause } catch (MMScriptException mex) { MyDialogUtils.showError(mex); } catch (Exception ex) { MyDialogUtils.showError(ex); } finally { // end of this acquisition (could be about to restart if separate viewers) try { // restore original window listeners try { vad.getImagePlus().getWindow().removeWindowListener(wl_acq); for (WindowListener l : wls_orig) { vad.getImagePlus().getWindow().addWindowListener(l); } } catch (Exception ex) { // do nothing, window is probably gone } if (cancelAcquisition_.get()) { ReportingUtils.logMessage("User stopped the acquisition"); } bq.put(TaggedImageQueue.POISON); // TODO: evaluate closeAcquisition call // at the moment, the Micro-Manager api has a bug that causes // a closed acquisition not be really closed, causing problems // when the user closes a window of the previous acquisition // changed r14705 (2014-11-24) // gui_.closeAcquisition(acqName); ReportingUtils.logMessage("diSPIM plugin acquisition " + acqName + " took: " + (System.currentTimeMillis() - acqButtonStart) + "ms"); // while(gui_.isAcquisitionRunning()) { // Thread.sleep(10); // ReportingUtils.logMessage("waiting for acquisition to finish."); // } // flag that we are done with acquisition acquisitionRunning_.set(false); // write acquisition settings if requested if (lastAcquisitionPath_ != null && prefs_.getBoolean(MyStrings.PanelNames.SETTINGS.toString(), Properties.Keys.PLUGIN_WRITE_ACQ_SETTINGS_FILE, false)) { String path = ""; try { path = lastAcquisitionPath_ + File.separator + "AcqSettings.txt"; PrintWriter writer = new PrintWriter(path); writer.println(acqSettingsJSON); writer.flush(); writer.close(); } catch (Exception ex) { MyDialogUtils.showError(ex, "Could not save acquisition settings to file as requested to path " + path); } } } catch (Exception ex) { // exception while stopping sequence acquisition, not sure what to do... MyDialogUtils.showError(ex, "Problem while finishing acquisition"); } } } // for loop over acquisitions // cleanup after end of all acquisitions // TODO be more careful and always do these if we actually started acquisition, // even if exception happened cameras_.setCameraForAcquisition(firstCameraKey, false); if (twoSided || acqBothCameras) { cameras_.setCameraForAcquisition(secondCameraKey, false); } // restore exposure times of SPIM cameras try { core_.setExposure(firstCamera, prefs_.getFloat(MyStrings.PanelNames.SETTINGS.toString(), Properties.Keys.PLUGIN_CAMERA_LIVE_EXPOSURE_FIRST.toString(), 10f)); if (twoSided || acqBothCameras) { core_.setExposure(secondCamera, prefs_.getFloat(MyStrings.PanelNames.SETTINGS.toString(), Properties.Keys.PLUGIN_CAMERA_LIVE_EXPOSURE_SECOND.toString(), 10f)); } gui_.refreshGUIFromCache(); } catch (Exception ex) { MyDialogUtils.showError("Could not restore exposure after acquisition"); } // reset channel to original if we clobbered it if (acqSettings.useChannels) { multiChannelPanel_.setConfig(originalChannelConfig); } // clean up controller settings after acquisition // want to do this, even with demo cameras, so we can test everything else // TODO figure out if we really want to return piezos to 0 position (maybe center position, // maybe not at all since we move when we switch to setup tab, something else??) controller_.cleanUpControllerAfterAcquisition(acqSettings.numSides, acqSettings.firstSideIsA, true); // if we did stage scanning restore its position and speed if (acqSettings.isStageScanning) { try { // make sure stage scanning state machine is stopped, otherwise setting speed/position won't take props_.setPropValue(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_STATE, Properties.Values.SPIM_IDLE); props_.setPropValue(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_MOTOR_SPEED, origXSpeed); props_.setPropValue(Devices.Keys.XYSTAGE, Properties.Keys.STAGESCAN_MOTOR_ACCEL, origXAccel); core_.setXYPosition(devices_.getMMDevice(Devices.Keys.XYSTAGE), xyPosUm.x, xyPosUm.y); } catch (Exception ex) { MyDialogUtils.showError("Could not restore XY stage position after acquisition"); } } updateAcquisitionStatus(AcquisitionStatus.DONE); posUpdater_.pauseUpdates(false); if (testAcq && prefs_.getBoolean(MyStrings.PanelNames.SETTINGS.toString(), Properties.Keys.PLUGIN_TESTACQ_SAVE, false)) { String path = ""; try { path = prefs_.getString(MyStrings.PanelNames.SETTINGS.toString(), Properties.Keys.PLUGIN_TESTACQ_PATH, ""); IJ.saveAs(acq_.getAcquisitionWindow().getImagePlus(), "raw", path); // TODO consider generating a short metadata file to assist in interpretation } catch (Exception ex) { MyDialogUtils.showError("Could not save raw data from test acquisition to path " + path); } } if (separateImageFilesOriginally) { ImageUtils.setImageStorageClass(TaggedImageStorageDiskDefault.class); } // restore camera try { core_.setCameraDevice(originalCamera); } catch (Exception ex) { MyDialogUtils.showError("Could not restore camera after acquisition"); } if (liveModeOriginally) { gui_.enableLiveMode(true); } if (nonfatalError) { MyDialogUtils.showError("Missed some images during acquisition, see core log for details"); } return true; } @Override public void saveSettings() { // save controller settings props_.setPropValue(Devices.Keys.PIEZOA, Properties.Keys.SAVE_CARD_SETTINGS, Properties.Values.DO_SSZ, true); props_.setPropValue(Devices.Keys.PIEZOB, Properties.Keys.SAVE_CARD_SETTINGS, Properties.Values.DO_SSZ, true); props_.setPropValue(Devices.Keys.GALVOA, Properties.Keys.SAVE_CARD_SETTINGS, Properties.Values.DO_SSZ, true); props_.setPropValue(Devices.Keys.GALVOB, Properties.Keys.SAVE_CARD_SETTINGS, Properties.Values.DO_SSZ, true); props_.setPropValue(Devices.Keys.PLOGIC, Properties.Keys.SAVE_CARD_SETTINGS, Properties.Values.DO_SSZ, true); } /** * Gets called when this tab gets focus. Refreshes values from properties. */ @Override public void gotSelected() { posUpdater_.pauseUpdates(true); props_.callListeners(); // old joystick associations were cleared when leaving // last tab so only do it if joystick settings need to be applied if (navigationJoysticksCB_.isSelected()) { updateJoysticks(); } sliceFrameAdvanced_.setVisible(advancedSliceTimingCB_.isSelected()); posUpdater_.pauseUpdates(false); } /** * called when tab looses focus. */ @Override public void gotDeSelected() { // if we have been using navigation panel's joysticks need to unset them if (navigationJoysticksCB_.isSelected()) { if (ASIdiSPIM.getFrame() != null) { ASIdiSPIM.getFrame().getNavigationPanel().doJoystickSettings(false); } } sliceFrameAdvanced_.setVisible(false); } @Override public void devicesChangedAlert() { devices_.callListeners(); } /** * Gets called when enclosing window closes */ @Override public void windowClosing() { if (acquisitionRequested_.get()) { cancelAcquisition_.set(true); while (acquisitionRunning_.get()) { // spin wheels until we are done } } sliceFrameAdvanced_.savePosition(); sliceFrameAdvanced_.dispose(); gridFrame_.savePosition(); gridFrame_.dispose(); } @Override public void refreshDisplay() { updateDurationLabels(); } @Override // Used to re-layout portion of window depending when camera mode changes, in // particular light sheet mode needs different set of controls. public void cameraModeChange() { CameraModes.Keys key = getSPIMCameraMode(); slicePanelContainer_.removeAll(); slicePanelContainer_.add((key == CameraModes.Keys.LIGHT_SHEET) ? lightSheetPanel_ : normalPanel_, "growx"); slicePanelContainer_.revalidate(); slicePanelContainer_.repaint(); } private void setRootDirectory(JTextField rootField) { File result = FileDialogs.openDir(null, "Please choose a directory root for image data", MMStudio.MM_DATA_SET); if (result != null) { rootField.setText(result.getAbsolutePath()); } } /** * The basic method for adding images to an existing data set. If the * acquisition was not previously initialized, it will attempt to initialize * it from the available image data. This version uses a blocking queue and is * much faster than the one currently implemented in the ScriptInterface * Eventually, this function should be replaced by the ScriptInterface version * of the same. * @param acq - MMAcquisition object to use (old way used acquisition name and then * had to call deprecated function on every call, now just pass acquisition object * @param frame - frame nr at which to insert the image * @param channel - channel at which to insert image * @param slice - (z) slice at which to insert image * @param position - position at which to insert image * @param ms - Time stamp to be added to the image metadata * @param taggedImg - image + metadata to be added * @param bq - Blocking queue to which the image should be added. This queue * should be hooked up to the ImageCache belonging to this acquisitions * @throws java.lang.InterruptedException * @throws org.micromanager.utils.MMScriptException */ private void addImageToAcquisition(MMAcquisition acq, int frame, int channel, int slice, int position, long ms, TaggedImage taggedImg, BlockingQueue<TaggedImage> bq) throws MMScriptException, InterruptedException { // verify position number is allowed if (acq.getPositions() <= position) { throw new MMScriptException("The position number must not exceed declared" + " number of positions (" + acq.getPositions() + ")"); } // verify that channel number is allowed if (acq.getChannels() <= channel) { throw new MMScriptException("The channel number must not exceed declared" + " number of channels (" + +acq.getChannels() + ")"); } JSONObject tags = taggedImg.tags; if (!acq.isInitialized()) { throw new MMScriptException("Error in the ASIdiSPIM logic. Acquisition should have been initialized"); } // create required coordinate tags try { MDUtils.setFrameIndex(tags, frame); tags.put(MMTags.Image.FRAME, frame); MDUtils.setChannelIndex(tags, channel); MDUtils.setChannelName(tags, channelNames_[channel]); MDUtils.setSliceIndex(tags, slice); MDUtils.setPositionIndex(tags, position); MDUtils.setElapsedTimeMs(tags, ms); MDUtils.setImageTime(tags, MDUtils.getCurrentTime()); MDUtils.setZStepUm(tags, zStepUm_); // save cached positions of SPIM head for this stack tags.put("SPIM_Position_X", xPositionUm_); // TODO consider computing accurate X position per slice for stage scanning data tags.put("SPIM_Position_Y", yPositionUm_); tags.put("SPIM_Position_Z", zPositionUm_); // NB this is SPIM head position, not position in stack if (!tags.has(MMTags.Summary.SLICES_FIRST) && !tags.has(MMTags.Summary.TIME_FIRST)) { // add default setting tags.put(MMTags.Summary.SLICES_FIRST, true); tags.put(MMTags.Summary.TIME_FIRST, false); } if (acq.getPositions() > 1) { // if no position name is defined we need to insert a default one if (tags.has(MMTags.Image.POS_NAME)) { tags.put(MMTags.Image.POS_NAME, "Pos" + position); } } // update frames if necessary if (acq.getFrames() <= frame) { acq.setProperty(MMTags.Summary.FRAMES, Integer.toString(frame + 1)); } } catch (JSONException e) { throw new MMScriptException(e); } bq.put(taggedImg); } /** * Gets position of sample in XYZ, being XY stage and SPIM head. Used to mark metadata with center position * that gets updated with multiple positions. */ private void refreshXYZPositions() { xPositionUm_ = positions_.getUpdatedPosition(Devices.Keys.XYSTAGE, Directions.X); // / will update cache for Y too yPositionUm_ = positions_.getCachedPosition(Devices.Keys.XYSTAGE, Directions.Y); zPositionUm_ = positions_.getUpdatedPosition(Devices.Keys.UPPERZDRIVE); } /***************** API *******************/ /** * @return true if an acquisition is currently underway * (e.g. all checks passed, controller set up, MM acquisition object created, etc.) */ public boolean isAcquisitionRunning() { return acquisitionRunning_.get(); } /** * @return true if an acquisition has been requested by user. Will * also return true if acquisition is running. */ public boolean isAcquisitionRequested() { return acquisitionRequested_.get(); } /** * Stops the acquisition by setting an Atomic boolean indicating that we should * halt. Does nothing if an acquisition isn't running. */ public void stopAcquisition() { if (isAcquisitionRequested()) { cancelAcquisition_.set(true); } } /** * @return pathname on filesystem to last completed acquisition * (even if it was stopped pre-maturely). Null if not saved to disk. */ public String getLastAcquisitionPath() { return lastAcquisitionPath_; } public String getLastAcquisitionName() { return lastAcquisitionName_; } public ij.ImagePlus getLastAcquisitionImagePlus() throws ASIdiSPIMException { try { return gui_.getAcquisition(lastAcquisitionName_).getAcquisitionWindow().getImagePlus(); } catch (MMScriptException e) { throw new ASIdiSPIMException(e); } } public String getSavingDirectoryRoot() { return rootField_.getText(); } public void setSavingDirectoryRoot(String directory) throws ASIdiSPIMException { rootField_.setText(directory); try { rootField_.commitEdit(); } catch (ParseException e) { throw new ASIdiSPIMException(e); } } public String getSavingNamePrefix() { return prefixField_.getText(); } public void setSavingNamePrefix(String acqPrefix) throws ASIdiSPIMException { prefixField_.setText(acqPrefix); try { prefixField_.commitEdit(); } catch (ParseException e) { throw new ASIdiSPIMException(e); } } public boolean getSavingSeparateFile() { return separateTimePointsCB_.isSelected(); } public void setSavingSeparateFile(boolean separate) { separateTimePointsCB_.setSelected(separate); } public boolean getSavingSaveWhileAcquiring() { return saveCB_.isSelected(); } public void setSavingSaveWhileAcquiring(boolean save) { saveCB_.setSelected(save); } public org.micromanager.asidispim.Data.AcquisitionModes.Keys getAcquisitionMode() { return (org.micromanager.asidispim.Data.AcquisitionModes.Keys) spimMode_.getSelectedItem(); } public void setAcquisitionMode(org.micromanager.asidispim.Data.AcquisitionModes.Keys mode) { spimMode_.setSelectedItem(mode); } public boolean getTimepointsEnabled() { return useTimepointsCB_.isSelected(); } public void setTimepointsEnabled(boolean enabled) { useTimepointsCB_.setSelected(enabled); } public int getNumberOfTimepoints() { return (Integer) numTimepoints_.getValue(); } public void setNumberOfTimepoints(int numTimepoints) throws ASIdiSPIMException { if (MyNumberUtils.outsideRange(numTimepoints, 1, 100000)) { throw new ASIdiSPIMException("illegal value for number of time points"); } numTimepoints_.setValue(numTimepoints); } // getTimepointInterval already existed public void setTimepointInterval(double intervalTimepoints) throws ASIdiSPIMException { if (MyNumberUtils.outsideRange(intervalTimepoints, 0.1, 32000)) { throw new ASIdiSPIMException("illegal value for time point interval"); } acquisitionInterval_.setValue(intervalTimepoints); } public boolean getMultiplePositionsEnabled() { return usePositionsCB_.isSelected(); } public void setMultiplePositionsEnabled(boolean enabled) { usePositionsCB_.setSelected(enabled); } public double getMultiplePositionsPostMoveDelay() { return PanelUtils.getSpinnerFloatValue(positionDelay_); } public void setMultiplePositionsDelay(double delayMs) throws ASIdiSPIMException { if (MyNumberUtils.outsideRange(delayMs, 0d, 10000d)) { throw new ASIdiSPIMException("illegal value for post move delay"); } positionDelay_.setValue(delayMs); } public boolean getChannelsEnabled() { return multiChannelPanel_.isMultiChannel(); } public void setChannelsEnabled(boolean enabled) { multiChannelPanel_.setPanelEnabled(enabled); } public String[] getAvailableChannelGroups() { return multiChannelPanel_.getAvailableGroups(); } public String getChannelGroup() { return multiChannelPanel_.getChannelGroup(); } public void setChannelGroup(String channelGroup) { String[] availableGroups = getAvailableChannelGroups(); for (String group : availableGroups) { if (group.equals(channelGroup)) { multiChannelPanel_.setChannelGroup(channelGroup); } } } public String[] getAvailableChannels() { return multiChannelPanel_.getAvailableChannels(); } public boolean getChannelEnabled(String channel) { ChannelSpec[] usedChannels = multiChannelPanel_.getUsedChannels(); for (ChannelSpec spec : usedChannels) { if (spec.config_.equals(channel)) { return true; } } return false; } public void setChannelEnabled(String channel, boolean enabled) { multiChannelPanel_.setChannelEnabled(channel, enabled); } // getNumSides() already existed public void setVolumeNumberOfSides(int numSides) { if (numSides == 2) { numSides_.setSelectedIndex(1); } else { numSides_.setSelectedIndex(0); } } public void setFirstSideIsA(boolean firstSideIsA) { if (firstSideIsA) { firstSide_.setSelectedIndex(0); } else { firstSide_.setSelectedIndex(1); } } public double getVolumeDelayBeforeSide() { return PanelUtils.getSpinnerFloatValue(delaySide_); } public void setVolumeDelayBeforeSide(double delayMs) throws ASIdiSPIMException { if (MyNumberUtils.outsideRange(delayMs, 0d, 10000d)) { throw new ASIdiSPIMException("illegal value for delay before side"); } delaySide_.setValue(delayMs); } public int getVolumeSlicesPerVolume() { return (Integer) numSlices_.getValue(); } public void setVolumeSlicesPerVolume(int slices) throws ASIdiSPIMException { if (MyNumberUtils.outsideRange(slices, 1, 65000)) { throw new ASIdiSPIMException("illegal value for number of slices"); } numSlices_.setValue(slices); } public double getVolumeSliceStepSize() { return PanelUtils.getSpinnerFloatValue(stepSize_); } public void setVolumeSliceStepSize(double stepSizeUm) throws ASIdiSPIMException { if (MyNumberUtils.outsideRange(stepSizeUm, 0d, 100d)) { throw new ASIdiSPIMException("illegal value for slice step size"); } stepSize_.setValue(stepSizeUm); } public boolean getVolumeMinimizeSlicePeriod() { return minSlicePeriodCB_.isSelected(); } public void setVolumeMinimizeSlicePeriod(boolean minimize) { minSlicePeriodCB_.setSelected(minimize); } public double getVolumeSlicePeriod() { return PanelUtils.getSpinnerFloatValue(desiredSlicePeriod_); } public void setVolumeSlicePeriod(double periodMs) throws ASIdiSPIMException { if (MyNumberUtils.outsideRange(periodMs, 1d, 1000d)) { throw new ASIdiSPIMException("illegal value for slice period"); } desiredSlicePeriod_.setValue(periodMs); } public double getVolumeSampleExposure() { return PanelUtils.getSpinnerFloatValue(desiredLightExposure_); } public void setVolumeSampleExposure(double exposureMs) throws ASIdiSPIMException { if (MyNumberUtils.outsideRange(exposureMs, 1.0, 1000.0)) { throw new ASIdiSPIMException("illegal value for sample exposure"); } desiredLightExposure_.setValue(exposureMs); } public boolean getAutofocusDuringAcquisition() { return useAutofocusCB_.isSelected(); } public void setAutofocusDuringAcquisition(boolean enable) { useAutofocusCB_.setSelected(enable); } public double getEstimatedSliceDuration() { return sliceTiming_.sliceDuration; } public double getEstimatedVolumeDuration() { return computeActualVolumeDuration(); } public double getEstimatedAcquisitionDuration() { return computeActualTimeLapseDuration(); } }