Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add contribution quick start, repeat acquisition, configuration and livestream #39

Merged
merged 22 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/contribution/docs_contribution_quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Quick Start Acquire **Docs** Contribution Guide

1. Make sure you have a fresh environment with the latest mkdocs and mkdocs-material installed. You can install them with `pip install -r requirements.txt` from the root of the repository.
2. Your pages should be written as markdown files, using the basic markdown syntax or following the [mkdocs](https://www.mkdocs.org/user-guide/writing-your-docs/) or [material for mkdocs](https://squidfunk.github.io/mkdocs-material/reference/formatting/) syntax.
3. Pages can be added to the top level menu or submenus by editing the `mkdocs.yml` file. The order of the pages in the menu is determined by the order of the pages in the `mkdocs.yml` file. Subpages can be added by creating subfolders in the `docs/` folder (see, for example, the `docs/tutorials/` folder).
4. To add images, place them in the `docs/images/` folder and reference them in your markdown files using the relative path `../images/your_image.png`.
5. Custom CSS configuration goes into the `docs/stylesheets/custom.css` file.
6. To build the website locally, after activating your environment (either using `conda activate <your-environment>` or `source activate <your-env>`, for example), run `mkdocs serve` to start a local server. You can then view the website at the URL indicated on your console.
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

Documentation for those looking to contribute to the Acquire project.

GitHub repositories: https://github.com/acquire-project
GitHub repositories: https://github.com/acquire-project
83 changes: 83 additions & 0 deletions docs/examples/livestream_napari.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""
This script will livestream data to the [napari viewer](https://napari.org/stable/). You may also utilize the `Acquire` napari plugin, which is provided in the `acquire-imaging` package on PyPI upon install. You can access the plugin in the napari plugins menu once `Acquire` is installed. You can review the [plugin code here](https://github.com/acquire-project/acquire-python/blob/main/python/acquire/__init__.py).
"""

import acquire
runtime = acquire.Runtime()

# Initialize the device manager
dm = runtime.device_manager()

# Grab the current configuration
config = runtime.get_configuration()

# Select the uniform random camera as the video source
config.video[0].camera.identifier = dm.select(acquire.DeviceKind.Camera, ".*random.*")

# Set the storage to trash to avoid saving the data
config.video[0].storage.identifier = dm.select(acquire.DeviceKind.Storage, "Trash")

# Set the time for collecting data for a each frame
config.video[0].camera.settings.exposure_time_us = 5e4 # 500 ms

config.video[0].camera.settings.shape = (300, 200)

# Set the max frame count to 100 frames
config.video[0].max_frame_count = 100

# Update the configuration with the chosen parameters
config = runtime.set_configuration(config)

# import napari and open a viewer to stream the data
import napari
viewer = napari.Viewer()

import time
from napari.qt.threading import thread_worker

def update_layer(args) -> None:
(new_image, stream_id) = args
print(f"update layer: {new_image.shape=}, {stream_id=}")
layer_key = f"Video {stream_id}"
try:
layer = viewer.layers[layer_key]
layer._slice.image._view = new_image
dgmccart marked this conversation as resolved.
Show resolved Hide resolved
layer.data = new_image
# you can use the private api with layer.events.set_data() to speed up by 1-2 ms/frame

except KeyError:
viewer.add_image(new_image, name=layer_key)

@thread_worker(connect={"yielded": update_layer})
def do_acquisition():
time.sleep(5)
runtime.start()

nframes = [0, 0]
stream_id = 0

def is_not_done() -> bool:
return (nframes[0] < config.video[0].max_frame_count) or (
nframes[1] < config.video[1].max_frame_count
)

def next_frame(): #-> Optional[npt.NDArray[Any]]:
"""Get the next frame from the current stream."""
if nframes[stream_id] < config.video[stream_id].max_frame_count:
if packet := runtime.get_available_data(stream_id):
n = packet.get_frame_count()
nframes[stream_id] += n
f = next(packet.frames())
return f.data().squeeze().copy()
return None

stream = 1
# loop to continue to update the data in napari while acquisition is running
while is_not_done():
if (frame := next_frame()) is not None:
yield frame, stream_id
time.sleep(0.1)

do_acquisition()

napari.run()
78 changes: 78 additions & 0 deletions docs/tutorials/configure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Configure an Acquisition

This tutorial will provide an in-depth explanation of setting configuration properites and demonstrate the relationships between various `Acquire` classes, such as `CameraProperties` and `StorageProperties`, used in the configuration process. In this example, we'll only configure one video source.

## Initialize `Runtime`

`Runtime` is the main entry point in Acquire. Through the runtime, you configure your devices, start acquisition, check acquisition status, inspect data as it streams from your cameras, and terminate acquisition. The `device_manager` method in `Runtime` creates an instance of the `DeviceManager` class. The `get_configuration` method in `Runtime` creates an instance of the `Properties` class. To configure the acquisition, we'll use those two methods to grab the configuration and to initialize a `DeviceManager` to set the attributes of `Properties` and related classes.

```python
import acquire

# Initialize a Runtime object
runtime = acquire.Runtime()

# Initialize the device manager
dm = runtime.device_manager()

# Grab the current configuration
config = runtime.get_configuration()
```

## Utilize `DeviceManager`

`DeviceManager` contains a `devices` method which creates a list of `DeviceIdentifier` objects each representing a discovered camera or storage device. Each `DeviceIdentifier` has an attribute `kind` that is a `DeviceKind` object, which has attributes specifying whether the device is a camera or storage device, as well as `Signals` and `StageAxes` attributes. The `Signals` and `StageAxes` attributes would apply to device kinds such as stages, which are not yet supported by `Acquire`.

`DeviceManager` has 2 methods for selecting devices for the camera and storage. For more information on these methods, check out the [Device Selection tutorial](https://acquire-project.github.io/acquire-docs/tutorials/select/). We'll use the `select` method in this example to choose a specific device.

```python
# Select the radial sine simulated camera as the video source
config.video[0].camera.identifier = dm.select(acquire.DeviceKind.Camera, "simulated: radial sin")

# Set the storage to Tiff
config.video[0].storage.identifier = dm.select(acquire.DeviceKind.Storage, "Tiff")
```

## `Properties` Class Explanation

Using `Runtime`'s `get_configuration` method we created `config`, an instance of the `Properties` class. `Properties` contains only one attribute `video` which is a tuple of `VideoStream` objects since `Acquire` currently supports 2 camera streaming. To configure the first video stream, we'll index this tuple to select the first `VideoStream` object `config.video[0]`.

`VideoStream` objects have 2 attributes `camera` and `storage` which are instances of the `Camera` and `Storage` classes, respectively, and will be used to set the attributes of the selected camera device `simulated: radial sin` and storage device `Tiff`. The other attributes of `VideoStream` are integers that specify the maximum number of frames to collect and how many frames to average, if any, before storing the data. The `frame_average_count` has a default value of `0`, which disables this feature.

## Configure `Camera`
`Camera` class objects have 2 attributes, `settings`, a `CameraProperties` object, and an optional attribute `identifier`, which is a `DeviceIdentifier` object.

`CameraProperties` has 5 attributes that are numbers and specify the exposure time and line interval in microseconds, how many pixels, if any, to bin (set to 1 by default to disable), and tuples for the image size and location on the camera chip. The other attributes are all instances of different classes. The `pixel_type` attribute is a `SampleType` object which indicates the data type of the pixel values in the image, such as Uint8. The `readout_direction` attribute is a `Direction` object specifying whether the data is read forwards or backwards from the camera. The `input_triggers` attribute is an `InputTriggers` object that details the characteristics of any input triggers in the system. The `output_triggers` attribute is an `OutputTriggers` object that details the characteristics of any output triggers in the system. All of the attributes of `InputTriggers` and `OutputTriggers` objects are instances of the `Trigger` class. The `Trigger` class is described in [this tutorial](https://acquire-project.github.io/acquire-docs/tutorials/trig_json/).

We'll configure some camera settings below.

```python
# Set the time for collecting data for a each frame
config.video[0].camera.settings.exposure_time_us = 5e4 # 50 ms

# (x, y) size of the image in pixels
config.video[0].camera.settings.shape = (1024, 768)

# Specify the pixel type as Uint32
config.video[0].camera.settings.pixel_type = acquire.SampleType.U32
```

## Configure `Storage`
`Storage` objects have 2 attributes, `settings`, a `StorageProperties` object, and an optional attribute `identifier`, which is an instance of the `DeviceIdentifier` class described above.

`StorageProperties` has 2 attributes `external_metadata_json` and `filename` which are strings of the filename or filetree of the output metadata in JSON format and image data in whatever format corresponds to the selected storage device, respectively. `first_frame_id` is an integer ID that corresponds to the first frame of the current acquisition and is typically 0. `pixel_scale_um` is the pixel size in microns. `enable_multiscale` is a boolean used to specify if the data should be saved as an image pyramid. See the [multiscale tutorial](https://acquire-project.github.io/acquire-docs/tutorials/multiscale/) for more information. The `chunking` attribute is an instance of the `ChunkingProperties` class, used for Zarr storage. See the [chunking tutorial](https://acquire-project.github.io/acquire-docs/tutorials/multiscale/) for more information.

We'll specify the name of the output image file below.

```python
# Set the output file to out.tiff
config.video[0].storage.settings.filename = "out.tiff"
```

# Update Configuration Settings
None of the configuration settings are updated in `Runtime` until the `set_configuration` method is called. We'll be creating a new `Properties` object with the `set_configuration` method. For simplicity, we'll reuse `config` for the name of that object as well, but note that `new_config = runtime.set_configuration(config)` also works here.

```python
# Update the configuration with the chosen parameters
config = runtime.set_configuration(config)
```
7 changes: 7 additions & 0 deletions docs/tutorials/livestream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Livestream to napari

The below script can be used to livestream data to the [napari viewer](https://napari.org/stable/). You may also utilize the `Acquire` napari plugin, which is provided in the package upon install. You can access the plugin in the napari plugins menu once `Acquire` is installed. You can review the [plugin code here](https://github.com/acquire-project/acquire-python/blob/main/python/acquire/__init__.py#L131). You may also stream using other packages such at `matplotlib`.

~~~python
{% include "../examples/livestream_napari.py" %}
~~~
85 changes: 85 additions & 0 deletions docs/tutorials/start_stop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Multiple Acquisitions

This tutorial will provide an example of starting, stopping, and restarting acquisition, or streaming from a video source.

## Configure Streaming

To start, we'll create a `Runtime` object and configure the streaming process. To do this, we'll utilize the setup method. More information on that method is detailed in [this tutorial](https://acquire-project.github.io/acquire-docs/tutorials/setup).

```python
import acquire

# Initialize a Runtime object
runtime = acquire.Runtime()

# Grarb Set Video Source and Storage Device
config = acquire.setup(runtime, "simulated: radial sin", "Tiff")

config.video[0].storage.settings.filename == "out.tif"
config.video[0].camera.settings.shape = (192, 108)
config.video[0].camera.settings.exposure_time_us = 10e4
config.video[0].max_frame_count = 10

# Update the configuration with the chosen parameters
config = runtime.set_configuration(config)
```

## Start, Stop, and Restart Acquisition

During Acquisition, the `AvailableData` object is the streaming interface. Upon shutdown, `Runtime` deletes all of the objects created during acquisition to free up resources, and you must stop acquisition by calling `runtime.stop()` between acquisitions. Otherwise, an exception will be raised.

To understand how acquisition works, we'll start, stop, and repeat acquisition and print the `DeviceState`, which can be `Armed`, `AwaitingConfiguration`, `Closed`, or `Running`, and the `AvailableData` object throughout the process.

If acquisition has ended, all of the objects are deleted, including `AvailableData` objects, so those will be `None` when not acquiring data. In addition, if enough time hasn't elapsed since acquisition started, `AvailableData` will also be `None`. We'll utilize the `time` python package to introduce time delays to account for these facts.

```python
# package used to introduce time delays
import time

# start acquisition
runtime.start()

print(runtime.get_state())
print(runtime.get_available_data(0))

# wait 0.5 seconds to allow time for data to be acquired
time.sleep(0.5)

print(runtime.get_state())
print(runtime.get_available_data(0))

# stop acquisition
runtime.stop()

print(runtime.get_state())
print(runtime.get_available_data(0))

# start acquisition
runtime.start()

# time delay of 5 seconds - acquisition only runs for 1 second
time.sleep(5)

print(runtime.get_state())
print(runtime.get_available_data(0))

# stop acquisition
runtime.stop()
```

The output will be:

```
DeviceState.Running
None
DeviceState.Running
<builtins.AvailableData object at 0x00000218D685E5B0>
DeviceState.Armed
None
DeviceState.Armed
<builtins.AvailableData object at 0x00000218D685E3D0>
```
1. The first time we print states is immediately after we started acqusition and enough time hasn't elapsed for data to be collected based on the exposure time, so the camera is running but there is no data yet.
2. The next print happens after waiting 0.5 seconds, so acquisition is still runnning and now there is acquired data available.
3. The subsequent print is following calling `runtime.stop()` which terminates acquisition after the specified max number of frames are collected, so the device is no longer running, although it is in the `Armed` state ready for acquisition, and there is no available data.
4. The final print occurs after waiting 5 seconds after starting acquisition, which is longer than the 1 second time needed to collect all the frames, so the device is no longer collecting data. However, `runtime.stop()` hasn't been called, so the `AvailableData` object has not yet been deleted.
2 changes: 1 addition & 1 deletion docs/tutorials/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@ config.video[0].storage.identifier = manager.select( acquire.DeviceKind.Storage,
config.video[0].storage.settings.filename = "out.tif"
```

Before proceeding, complete the `Camera` setup and call `runtime.set_configuration(config)` to save those new configuration settings.
Before proceeding, complete the `Camera` setup and call `set_configuration` to save those new configuration settings.
dgmccart marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 6 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@ nav:
- Tutorials:
- tutorials/tutorials.md
- tutorials/trigger.md
- tutorials/configure.md
- tutorials/framedata.md
- tutorials/start_stop.md
- tutorials/livestream.md
- tutorials/select.md
- tutorials/props_json.md
- tutorials/trig_json.md
- tutorials/drivers.md
- tutorials/storage.md
- For contributors: for_contributors.md
- For contributors:
- contribution/for_contributors.md
- contribution/docs_contribution_quickstart.md

markdown_extensions:
- pymdownx.highlight:
Expand Down