ScanImage Scripting API

ScanImage is fully scriptable using Matlab’s command window and/or user defined functions. This article describes the ScanImage Application Programming Interface (API) and how to use it to automate ScanImage and extend ScanImage’s functionality.

ScanImage Architecture

The ScanImage architecture is based on the Model-View-Controller design pattern. The application’s logic and hardware control is accessed via hSI, the ScanImage handle.

hSI grants the user access to the ScanImage components, the building blocks of an acquisition. This includes Matlab handles to hardware devices (PMTs, shutters, beam modulators, etc.), configuration managers (ROI manager, waveform manager, motion, etc.), imaging systems, etc.

ScanImage’s GUI is accessible through the handle hSIGui, which is split up into a left panel, middle panel, and right panel.

The left panel contains the basic acquisition settings and more advanced options are available depending on experiment requirements. The middle panel contains the viewport (where the acquisition can be visualized in space) and the auxiliary panel (for configuring and using helpful features). The right panel stores the widget bar for quick visual device information and access.


Basic Functionality

hSI lives in the base workspace whenever ScanImage is running. Everything below assumes it is in scope; inside a user function you get it from src.hSI.

hSI.acqState                     % 'idle', 'focus', 'grab', 'loop', 'loop_wait', 'point'
hSI.hScan2D.name                 % the active imaging system
hSI.hRoiManager.scanFrameRate    % frames per second at the current settings

hSI.startFocus();                % live view, nothing logged
hSI.abort();

hSI.startGrab();                 % one acquisition
hSI.startLoop();                 % hSI.acqsPerLoop acquisitions

The start methods return immediately once the acquisition is armed. They do not block until it finishes - to run code at the end of an acquisition, hook the acqModeDone event rather than polling.

See also

hSI: Acquisition Control for the full set of properties and methods on hSI itself, and Component API for the components underneath it.


Recipes

Configure and run a logged acquisition

The properties are spread over several components: resolution and zoom on the ROI manager, file names on the imaging system, the logging switch and channel selection on the channels component.

 1% geometry and resolution
 2hSI.hRoiManager.pixelsPerLine  = 512;
 3hSI.hRoiManager.linesPerFrame  = 512;
 4hSI.hRoiManager.scanZoomFactor = 2;
 5
 6% channels
 7hSI.hChannels.channelDisplay = [1 2];
 8hSI.hChannels.channelSave    = [1 2];
 9
10% where the files go
11hSI.hScan2D.logFilePath    = 'C:\data\2026-09-04';
12hSI.hScan2D.logFileStem    = 'mouse01_fov1';
13hSI.hScan2D.logFileCounter = 1;
14hSI.hChannels.loggingEnable = true;
15
16% how much to acquire
17hSI.hStackManager.enable     = false;
18hSI.hStackManager.framesPerSlice = 500;
19
20hSI.startGrab();

Note

Not every property can be changed while an acquisition is running. Setting one that cannot be during a focus makes ScanImage abort, apply, and restart the focus; during a grab or loop the change is refused with a message. See Component Framework.

Acquire a fast volume

 1hSI.hStackManager.enable          = true;
 2hSI.hStackManager.stackMode       = scanimage.types.StackMode.fast;
 3hSI.hStackManager.stackActuator   = scanimage.types.StackActuator.fastZ;
 4hSI.hStackManager.stackDefinition = scanimage.types.StackDefinition.uniform;
 5hSI.hStackManager.numSlices       = 30;
 6hSI.hStackManager.stackZStepSize  = 3;      % microns
 7hSI.hStackManager.numVolumes      = 100;
 8
 9hSI.hFastZ.enable      = true;
10hSI.hFastZ.flybackTime = 10e-3;
11
12fprintf('%.2f volumes/s\n',hSI.hRoiManager.scanVolumeRate);
13hSI.startGrab();

Run code on every frame

Register a user function on frameAcquired. Guard on endOfFrame - with striping enabled the event can fire on a partial frame.

 1function meanIntensityUserFcn(src,evt,varargin)
 2    hSI = src.hSI;
 3    sd  = hSI.hDataManager.lastStripeData;
 4
 5    if isempty(sd) || ~sd.endOfFrame
 6        return
 7    end
 8
 9    for idx = 1:numel(sd.roiData)
10        hRd   = sd.roiData{idx};
11        chIdx = find(hRd.channels == 1);
12        if isempty(chIdx); continue; end
13
14        img = hRd.imageData{chIdx}{1}.';    % channel first, z second, transposed
15        fprintf('frame %d  roi %s  mean %.1f\n', ...
16            sd.frameNumberAcqMode, hRd.hRoi.name, mean(img(:)));
17    end
18end

Register it from the command window:

s = struct('EventName','frameAcquired','UserFcnName','meanIntensityUserFcn', ...
           'Arguments',{{}},'Enable',true);
hSI.hUserFunctions.userFunctionsCfg(end+1) = s;

Warning

This runs on the acquisition path. Keep it short, and watch sd.stripesRemaining - if it grows, your callback is too slow and frames will be dropped. Buffer and process on a timer instead.

See also

Acquisition Data Structures for the shapes of StripeData and RoiData.

Move the stage and image a list of positions

 1positions = [0 0 0; 200 0 0; 200 200 0; 0 200 0];   % sample-relative microns
 2
 3for idx = 1:size(positions,1)
 4    hSI.hMotors.moveSample(positions(idx,:));       % blocking
 5
 6    hSI.hScan2D.logFileStem = sprintf('tile_%02d',idx);
 7    hSI.startGrab();
 8
 9    while ~strcmpi(hSI.acqState,'idle')
10        pause(0.05);
11    end
12end

For anything beyond a handful of positions, use hSI.hTileManager or hSI.hCycleManager rather than a loop like this - they handle settling, ordering and metadata.

Convert a point between coordinate systems

Any position in ScanImage is a Points object plus the coordinate system it lives in. Converting is one call.

 1hCSs = hSI.hCoordinateSystems;
 2
 3% a point 1 optical degree off-center in the current scanner's space
 4hPt = scanimage.mroi.coordinates.Points(hSI.hScan2D.hCSZAffineLut,[1 0 0]);
 5
 6hPtRef    = hPt.transform(hCSs.hCSReference);        % optical degrees, um in z
 7hPtSample = hPt.transform(hCSs.hCSSampleRelative);   % microns
 8
 9fprintf('that point is at %.1f %.1f %.1f um in the sample\n',hPtSample.points);
10
11% drive the stage so that point sits under the objective
12hSI.hMotors.move(hPtSample);

See also

Coordinate System API, and ScanImage Coordinate System Instances for what each named space means.

Build an MROI ROI group

 1hRoiGroup = scanimage.mroi.RoiGroup();
 2
 3for idx = 1:3
 4    hSf = scanimage.mroi.scanfield.fields.RotatedRectangle();
 5    hSf.centerXY           = [(idx-2)*2, 0];
 6    hSf.sizeXY             = [1.5 1.5];
 7    hSf.pixelResolutionXY  = [256 256];
 8    hSf.rotationDegrees    = 0;
 9
10    hRoi = scanimage.mroi.Roi();
11    hRoi.add(0,hSf);                 % scanfield at z = 0
12
13    hRoiGroup.add(hRoi);
14end
15
16hSI.hRoiManager.roiGroupMroi = hRoiGroup;
17hSI.hRoiManager.mroiEnable   = true;

Drive an output from an integration ROI

Online analysis with a closed loop: the integration ROI manager computes traces, a post-processing function transforms them, and the configured output channels emit them.

1hSI.hIntegrationRoiManager.enable        = true;
2hSI.hIntegrationRoiManager.postProcessFcn = @thresholdPostProcess;
3
4function integrationValues = thresholdPostProcess(rois,integrationDone,arrayIndices, ...
5        integrationValueHistory,integrationTimestampHistory,integrationFrameNumberHistory)
6    integrationValues = integrationValueHistory(arrayIndices);
7    integrationValues = double(integrationValues > 100);   % simple threshold
8end

Batch process logged files

The readers give you the header as a struct whose field names mirror the live property paths, so analysis code reads the same way whether it is live or offline.

 1files = dir('C:\data\2026-09-04\*.tif');
 2
 3for idx = 1:numel(files)
 4    fname = fullfile(files(idx).folder,files(idx).name);
 5
 6    % header only - fast, does not read image data
 7    header = scanimage.util.opentif(fname);
 8
 9    fprintf('%s: %dx%d px, %.2f fps, %d slices\n', fname, ...
10        header.SI.hRoiManager.pixelsPerLine, ...
11        header.SI.hRoiManager.linesPerFrame, ...
12        header.SI.hRoiManager.scanFrameRate, ...
13        header.SI.hStackManager.actualNumSlices);
14end
15
16% MROI data with its ROI geometry
17[roiData,roiGroup,header] = scanimage.util.getMroiDataFromTiff(fname);

See also

Acquisition Metadata for the full header map and the list of readers.

Talk to a device directly

Every device is a named resource. This works with ScanImage running or closed.

 1hRS = dabs.resources.ResourceStore();
 2
 3% everything in an error state
 4hBad = hRS.filter(@(h)~isempty(h.errorMsg));
 5cellfun(@(h)fprintf('%s: %s\n',h.name,h.errorMsg),hBad);
 6
 7% a specific PMT, through the device API rather than the component
 8hPmt = hRS.filterByName('PMT1');
 9hPmt.setGain(0.55);
10hPmt.queryStatus();
11fprintf('gain %.2f V, tripped %d\n',hPmt.gain_V,hPmt.tripped);

Warning

Writing to a channel or device that ScanImage has reserved fights with whatever is driving it. Check hResource.reserved first, and prefer doing this while ScanImage is idle.

See also

dabs Device API.

React to a change instead of polling

Component properties are observable. Use most.ErrorHandler.addCatchingListener rather than addlistener, so an error in your callback cannot break the acquisition.

1hL = most.ErrorHandler.addCatchingListener(hSI.hStackManager,'slicesDone','PostSet', ...
2     @(src,evt)fprintf('slice %d of %d\n', ...
3         hSI.hStackManager.slicesDone,hSI.hStackManager.actualNumSlices));
4
5% ... later
6delete(hL);

Some components offer a debounced event that is cheaper than a PostSet listener - prefer hSI.hMotors’s samplePositionChanged and hSI.hScan2D’s scannersetChanged where they exist.

See also

The most Framework.