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();
See also
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;
See also
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
See also
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
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