Skip to main content

Smart Microscopy Workflows with Jupyter Notebooks

Jupyter notebooks provide an ideal environment for developing interactive microscopy workflows, combining live hardware control, real-time data analysis, and visualization in a single interface.

Overview

ImSwitch + Jupyter enables:

  • Interactive Experiments: Real-time parameter adjustment
  • Live Data Analysis: Process images as they're acquired
  • Workflow Documentation: Combine code, results, and explanations
  • Reproducible Research: Share complete experimental protocols
  • Educational Tools: Interactive learning environments

Setup and Installation

Jupyter Environment Setup

# Install Jupyter with ImSwitch integration
pip install jupyter jupyterlab ipywidgets
pip install matplotlib seaborn plotly
pip install scikit-image opencv-python napari

# Enable Jupyter widgets
jupyter nbextension enable --py widgetsnbextension
jupyter labextension install @jupyter-widgets/jupyterlab-manager

# For napari integration
pip install napari[all]

ImSwitch Integration

ImSwitch Integration

ImSwitch provides a powerful REST API that can be accessed from Jupyter notebooks using the imswitchclient library. This enables remote control of microscopy hardware and creates interactive experimental workflows.

ImSwitchClient Library

The imswitchclient provides a Python wrapper for the ImSwitch REST API, enabling:

  • Remote Control: Interface with ImSwitch through REST API endpoints
  • Comprehensive Access: Control positioners, lasers, detectors, and imaging settings
  • Interactive Exploration: FastAPI Swagger UI at http://localhost:8001/docs
  • Modular Design: Separate managers for different hardware components

Installation:

# Install from PyPI
!pip install imswitchclient

# Or install latest development version
!pip install git+https://github.com/openUC2/imswitchclient.git

Basic Connection Setup

from imswitchclient import ImSwitchClient
import numpy as np
import matplotlib.pyplot as plt
from IPython.display import display, clear_output
import ipywidgets as widgets

# Connect to ImSwitch instance
# For local instance:
client = ImSwitchClient(host="localhost", port=8001)

# For remote instance (e.g., Raspberry Pi):
# client = ImSwitchClient(host="192.168.1.100", port=8001)

# Check connection
if client.is_connected():
print("✓ Connected to ImSwitch")
print(f"Available positioners: {client.positionersManager.getAllDeviceNames()}")
print(f"Available detectors: {client.detectorsManager.getAllDeviceNames()}")
else:
print("✗ Connection failed")

Interactive Hardware Control Widgets

# Create interactive widgets for hardware control
def create_stage_control_widget():
"""Create interactive stage control widget"""

# Get available positioners
positioners = client.positionersManager.getAllDeviceNames()
if not positioners:
print("No positioners available")
return

positioner_name = positioners[0]

# Create widgets
x_slider = widgets.FloatSlider(min=-5000, max=5000, step=100, description='X Position:')
y_slider = widgets.FloatSlider(min=-5000, max=5000, step=100, description='Y Position:')
z_slider = widgets.FloatSlider(min=-1000, max=1000, step=10, description='Z Position:')

move_button = widgets.Button(description="Move Stage", button_style='success')
home_button = widgets.Button(description="Home All", button_style='warning')

# Position display
position_output = widgets.Output()

def move_stage(b):
"""Move stage to widget positions"""
with position_output:
clear_output(wait=True)
try:
# Move to absolute positions
client.positionersManager.move(
positioner_name,
"X",
x_slider.value,
absolute=True
)
client.positionersManager.move(
positioner_name,
"Y",
y_slider.value,
absolute=True
)
client.positionersManager.move(
positioner_name,
"Z",
z_slider.value,
absolute=True
)
print(f"✓ Moved to X={x_slider.value}, Y={y_slider.value}, Z={z_slider.value}")
except Exception as e:
print(f"✗ Move failed: {e}")

def home_stage(b):
"""Home all stage axes"""
with position_output:
clear_output(wait=True)
try:
# Home all axes (implementation depends on hardware)
print("🏠 Homing stage...")
# client.positionersManager.home(positioner_name)
print("✓ Homing complete")
except Exception as e:
print(f"✗ Homing failed: {e}")

move_button.on_click(move_stage)
home_button.on_click(home_stage)

# Layout widgets
controls = widgets.VBox([
widgets.HTML("<h3>Stage Control</h3>"),
x_slider, y_slider, z_slider,
widgets.HBox([move_button, home_button]),
position_output
])

return controls

# Display stage control widget
stage_widget = create_stage_control_widget()
display(stage_widget)

Live Imaging Widget

def create_live_imaging_widget():
"""Create live imaging interface"""

# Get available detectors
detectors = client.detectorsManager.getAllDeviceNames()
if not detectors:
print("No detectors available")
return

detector_name = detectors[0]

# Create widgets
exposure_slider = widgets.FloatSlider(
min=1, max=1000, value=100, step=10,
description='Exposure (ms):'
)

gain_slider = widgets.FloatSlider(
min=1, max=10, value=1, step=0.1,
description='Gain:'
)

capture_button = widgets.Button(description="Capture Image", button_style='primary')
live_button = widgets.ToggleButton(description="Live View", button_style='info')

# Image display
image_output = widgets.Output()

def capture_image(b):
"""Capture single image"""
with image_output:
clear_output(wait=True)
try:
# Set camera parameters
client.detectorsManager.setParameter(detector_name, 'ExposureTime', exposure_slider.value)
client.detectorsManager.setParameter(detector_name, 'Gain', gain_slider.value)

# Capture image
image = client.recordingManager.snapImage()

# Display image
plt.figure(figsize=(8, 6))
plt.imshow(image, cmap='gray')
plt.title(f'Captured Image - Exposure: {exposure_slider.value}ms, Gain: {gain_slider.value}')
plt.colorbar()
plt.show()

except Exception as e:
print(f"✗ Capture failed: {e}")

def toggle_live_view(change):
"""Toggle live view"""
if change['new']: # Live view enabled
print("🔴 Live view starting...")
# Implementation for live view would go here
else: # Live view disabled
print("⏹️ Live view stopped")

capture_button.on_click(capture_image)
live_button.observe(toggle_live_view, names='value')

# Layout widgets
controls = widgets.VBox([
widgets.HTML("<h3>Live Imaging</h3>"),
exposure_slider, gain_slider,
widgets.HBox([capture_button, live_button]),
image_output
])

return controls

# Display imaging widget
imaging_widget = create_live_imaging_widget()
display(imaging_widget)

Automated Workflows

def z_stack_acquisition(start_z, end_z, step_z, exposure_time=100):
"""Automated Z-stack acquisition"""

print("🔬 Starting Z-stack acquisition...")

# Calculate Z positions
z_positions = np.arange(start_z, end_z + step_z, step_z)
images = []

try:
# Set camera parameters
detector_name = client.detectorsManager.getAllDeviceNames()[0]
positioner_name = client.positionersManager.getAllDeviceNames()[0]

client.detectorsManager.setParameter(detector_name, 'ExposureTime', exposure_time)

# Progress bar
progress = widgets.IntProgress(
value=0, min=0, max=len(z_positions),
description='Z-stack:'
)
display(progress)

for i, z_pos in enumerate(z_positions):
# Move to Z position
client.positionersManager.move(positioner_name, "Z", z_pos, absolute=True)

# Wait for settling
import time
time.sleep(0.5)

# Capture image
image = client.recordingManager.snapImage()
images.append(image)

# Update progress
progress.value = i + 1
progress.description = f'Z-stack: {z_pos:.1f}μm'

print(f"✓ Z-stack complete: {len(images)} images acquired")

# Display results
fig, axes = plt.subplots(1, min(5, len(images)), figsize=(15, 3))
if len(images) == 1:
axes = [axes]

for i, ax in enumerate(axes):
if i < len(images):
ax.imshow(images[i], cmap='gray')
ax.set_title(f'Z={z_positions[i]:.1f}μm')
ax.axis('off')

plt.tight_layout()
plt.show()

return images, z_positions

except Exception as e:
print(f"✗ Z-stack failed: {e}")
return None, None

# Example usage
images, positions = z_stack_acquisition(start_z=-100, end_z=100, step_z=50)

Time-lapse Imaging

def time_lapse_acquisition(duration_minutes=10, interval_seconds=30):
"""Automated time-lapse acquisition"""

print(f"⏱️ Starting time-lapse: {duration_minutes} min, every {interval_seconds}s")

import time

total_frames = int((duration_minutes * 60) / interval_seconds)
images = []
timestamps = []

try:
detector_name = client.detectorsManager.getAllDeviceNames()[0]

# Progress bar
progress = widgets.IntProgress(
value=0, min=0, max=total_frames,
description='Time-lapse:'
)
display(progress)

start_time = time.time()

for frame in range(total_frames):
# Capture image
image = client.recordingManager.snapImage()
current_time = time.time() - start_time

images.append(image)
timestamps.append(current_time)

# Update progress
progress.value = frame + 1
progress.description = f'Frame {frame+1}/{total_frames}'

# Wait for next interval
if frame < total_frames - 1: # Don't wait after last frame
time.sleep(interval_seconds)

print(f"✓ Time-lapse complete: {len(images)} frames acquired")

# Display sample frames
sample_indices = np.linspace(0, len(images)-1, min(5, len(images)), dtype=int)

fig, axes = plt.subplots(1, len(sample_indices), figsize=(15, 3))
if len(sample_indices) == 1:
axes = [axes]

for i, idx in enumerate(sample_indices):
axes[i].imshow(images[idx], cmap='gray')
axes[i].set_title(f't={timestamps[idx]/60:.1f}min')
axes[i].axis('off')

plt.tight_layout()
plt.show()

return images, timestamps

except Exception as e:
print(f"✗ Time-lapse failed: {e}")
return None, None

# Example usage
# images, times = time_lapse_acquisition(duration_minutes=5, interval_seconds=10)

Google Colab Integration

The ImSwitchClient works seamlessly with Google Colab, enabling cloud-based microscopy control:

Try these examples:

Open In Colab - Basic ImSwitch Control

Open In Colab - Stage Calibration

Colab Setup:

# Install ImSwitchClient in Colab
!pip install imswitchclient

# Connect to remote ImSwitch instance
from imswitchclient import ImSwitchClient

# Replace with your microscope's IP address
MICROSCOPE_IP = "192.168.1.100" # Your Raspberry Pi IP
client = ImSwitchClient(host=MICROSCOPE_IP, port=8001)

Advanced Workflows

# Multi-position imaging
def multi_position_imaging(positions, z_stack_params=None):
"""Acquire images at multiple XY positions"""

positioner_name = client.positionersManager.getAllDeviceNames()[0]
all_images = []

for i, (x, y) in enumerate(positions):
print(f"📍 Position {i+1}/{len(positions)}: ({x}, {y})")

# Move to position
client.positionersManager.move(positioner_name, "X", x, absolute=True)
client.positionersManager.move(positioner_name, "Y", y, absolute=True)

# Optional Z-stack at each position
if z_stack_params:
images, _ = z_stack_acquisition(**z_stack_params)
all_images.append(images)
else:
image = client.recordingManager.snapImage()
all_images.append(image)

return all_images

# Define positions (in stage units)
positions = [(0, 0), (1000, 0), (0, 1000), (1000, 1000)]

# Acquire images
# images = multi_position_imaging(positions)

This comprehensive integration enables powerful, interactive microscopy workflows directly from Jupyter notebooks, whether running locally, on Google Colab, or other cloud platforms.

  • Open Source: Inspired by OpenFlexure Client, freely available under the MIT license.

Installation

You can install ImSwitchClient via pip:

pip install imswitchclient

Getting Started

Initializing the Client

import imswitchclient.ImSwitchClient as imc

# Initialize the client
client = imc.ImSwitchClient(host="0.0.0.0", isHttps=True, port=8001)

Example: Moving a Stage and Acquiring an Image

import numpy as np
import matplotlib.pyplot as plt
import time

# Retrieve positioner names
positioner_names = client.positionersManager.getAllDeviceNames()
positioner_name = positioner_names[0]

# Get current position
current_positions = client.positionersManager.getPositionerPositions()[positioner_name]
initial_position = (current_positions["X"], current_positions["Y"])

# Turn on illumination
laser_name = client.lasersManager.getLaserNames()[0]
client.lasersManager.setLaserActive(laser_name, True)
client.lasersManager.setLaserValue(laser_name, 512)

# Move the stage and capture an image
def capture_image_at_position(x, y):
client.positionersManager.movePositioner(positioner_name, "X", x, is_absolute=True, is_blocking=True)
client.positionersManager.movePositioner(positioner_name, "Y", y, is_absolute=True, is_blocking=True)
last_frame = client.recordingManager.snapNumpyToFastAPI()
plt.imshow(last_frame)
plt.show()

# Example scanning
for ix in range(5):
for iy in range(5):
new_x = initial_position[0] + ix * 50
new_y = initial_position[1] + iy * 50
capture_image_at_position(new_x, new_y)

# Return stage to initial position
client.positionersManager.movePositioner(positioner_name, "X", initial_position[0], is_absolute=True, is_blocking=True)
client.positionersManager.movePositioner(positioner_name, "Y", initial_position[1], is_absolute=True, is_blocking=True)

Laser Control Example

laser_name = client.lasersManager.getLaserNames()[0]
client.lasersManager.setLaserActive(laser_name, True)
client.lasersManager.setLaserValue(laser_name, 800)

# Verify laser status
print(client.lasersManager.getLaserNames())
client.lasersManager.setLaserActive(laser_name, False)

Recording an Image

# Take a snapshot
image = client.recordingManager.snapNumpyToFastAPI()
plt.imshow(image)
plt.show()

Setting Live View

client.viewManager.setLiveViewActive(True)
client.viewManager.setLiveViewCrosshairVisible(True)
client.viewManager.setLiveViewGridVisible(False)

API Overview

The ImSwitch API provides access to various components:

Positioners Manager

  • getAllDeviceNames() - Get all available positioners.
  • getPositionerPositions() - Get current position.
  • movePositioner(name, axis, value, is_absolute, is_blocking) - Move the stage.
  • homeAxis(name, axis, is_blocking) - Home the positioner.

Lasers Manager

  • getLaserNames() - Get available lasers.
  • setLaserActive(name, status) - Turn laser on/off.
  • setLaserValue(name, value) - Set laser intensity.

Recording Manager

  • snapNumpyToFastAPI() - Capture an image.
  • startRecording() - Begin recording.
  • stopRecording() - Stop recording.

View Manager

  • setLiveViewActive(status) - Enable live view.
  • setLiveViewCrosshairVisible(status) - Show/hide crosshair.
  • setLiveViewGridVisible(status) - Show/hide grid.

Contributing

Contributions are welcome! Visit the GitHub repository for details: https://github.com/openUC2/imswitchclient

License

This project is licensed under the MIT License.