Python integration

Python integration

Advanced

GA3 Python nodes support a wide range of Python packages, enabling advanced data analysis, visualization, and computational capabilities. By integrating scientific libraries directly into GA3 recipes, the platform’s functionality can be extended to meet specific research needs. Whether performing statistical analysis, processing images, analyzing large datasets, or applying machine learning techniques, Python nodes offer the flexibility to enhance workflows with customized scripts.

This section provides examples of how various scientific Python packages can be used within GA3, helping to streamline research and maximize the available tools.

For more GA3 examples, check the Laboratory Imaging GitHub repository GA3-examples.

NIS-Express comes with many scientific packages preinstalled. In order to install additional python modules use separate environments or managed environment in the Python node (see below).

NumPy for simple threshold

This workflows showcases a simple working example of using NumPy library for segmentation (see numpy.org).

Setup

Open the file that needs to be segmented.

In NIS Express GA3 Editor:

  • Add Python node and open its settings
    • Add color input
    • Add binary output
    • Insert code from Code section.
  • Connect the input pin to the Channels output to be segmented.
  • Connect the output pin to the SaveBinariesHDF5 node.

Code

GA3 Python Editor
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# IMPORTANT: 'limnode' must be imported like this (not from nor as)
import limnode
import numpy as np

# defines output parameter properties
def output(inp: limnode.InDefTuple, out: limnode.OutDefTuple, par: limnode.UserParTuple) -> None:
    out[0].makeNew("Binary", "#00ff00")

# return Program for dimension reduction or two-pass processing
def build(par: limnode.UserParTuple, loops: limnode.LoopDefs) -> limnode.Program|None:
    return None

# called for each frame/volume
def run(inp: limnode.InDataTuple, out: limnode.OutDataTuple, par: limnode.UserParTuple, ctx: limnode.RunContext) -> None:
    img = inp[0].data
    bin = np.where((img >= 300) & (img <= 400), 1, 0)
    out[0].data[:] = bin.astype(np.uint8)

# child process initialization (when outproc is set)
if __name__ == '__main__':
    from limnode import print
    limnode.child_main(run, output, build)

The code:

  1. Imports NumPy. [line 3]
  2. Names binary output as “Binary” and sets its color green. [line 7]
  3. Get color data. [line 15]
  4. Segments values in range from 300 to 400 (edit as needed) [line 16]
  5. Stores data to output as unsigned 8bit integer. [line 17]

Results

scikit-image for segmentation

Scikit-image is “a collection of algorithms for image processing” (see scikit-image.org).

Setup

Open the file that needs to be segmented.

In NIS Express GA3 Editor:

  • Add Python node and open its settings
    • Add color input
    • Add binary output
    • Insert code from Code section.
  • Connect the input pin to the Channels output to be segmented.
  • Connect the output pin to the SaveBinariesHDF5 node.

Code

GA3 Python Editor
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# IMPORTANT: 'limnode' must be imported like this (not from nor as)
import limnode, numpy
from skimage import filters

# defines output parameter properties
def output(inp: limnode.InDefTuple, out: limnode.OutDefTuple, par: limnode.UserParTuple) -> None:
    out[0].makeNew("otsu", (0, 255, 255))

# return Program for dimension reduction or two-pass processing
def build(par: limnode.UserParTuple, loops: limnode.LoopDefs) -> limnode.Program|None:
    return None

# called for each frame/volume
def run(inp: limnode.InDataTuple, out: limnode.OutDataTuple, par: limnode.UserParTuple, ctx: limnode.RunContext) -> None:
    image = inp[0].data[0, :, :, 0]
    threshold = filters.threshold_otsu(image)
    binary = image > threshold
    out[0].data[0, :, :, 0] = binary.astype(numpy.uint8)

# child process initialization (when outproc is set)
if __name__ == '__main__':
    from limnode import print
    limnode.child_main(run, output, build)
  1. Imports NumPy and filters from the scikit-image package [lines 2, 3].
  2. Defines the output to be a new binary named “otsu” and sets cyan color. [line 7].
  3. Takes the image from the inp array [line 15].
  4. Calculates the threshold calling threshold_otsu [line 16].
  5. Creates the binary by directly comparing the values from the image with the threshold value [line 17].
  6. Sets the binary data into the out array while converting to the proper format (uint8 or int32 for binary IDs) [line 18].

Results

BaSiCPy for shading removal

BaSiCPy is a library “for background and shading correction of optical microscopy images” (see BaSiCPy).

Setup

Open the file that needs to be processed.

In NIS Express GA3 Editor:

  • Add Channel input.
  • Add Channel output.
  • Set the python mode to “Managed environment”
  • Insert the environment from the Environment section.
  • Insert the code from Code section.
  • Connect the input pin to the Channels output to be processed.
  • Connect the output pin to the SavePictures node.

GA3 nodes

Environment

channels:
    - conda-forge
dependencies:
    - python=3.10
    - pip
    - pip:
        - "scipy<1.13"
        - "jax[cpu]==0.4.13"
        - "hyperactive<5"
        - "ml-dtypes==0.2.0"
        - basicpy
variables:
  PIP_FIND_LINKS: "https://whls.blob.core.windows.net/unstable/index.html"
  PIP_USE_DEPRECATED: "legacy-resolver"

The environment was created and tested in the November, 2025.

Consult with LLMs (e.g. ChatGPT) in case the versions do not work anymore.

GA3 nodes

Code

GA3 Python editor
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# IMPORTANT: 'limnode' must be imported like this (not from nor as)
import limnode, numpy
from basicpy import BaSiC

basic = BaSiC(get_darkfield=True)
fitted: bool = False
imgcache: list[numpy.ndarray] = []

# defines output parameter properties
def output(inp: limnode.InDefTuple, out: limnode.OutDefTuple, par: limnode.UserParTuple) -> None:
   pass

# return Program for dimension reduction or two-pass processing
def build(par: limnode.UserParTuple, loops: limnode.LoopDefs) -> limnode.Program|None:
   return limnode.TwoPassProgram(loops).overMultiPoint()

# called for each frame/volume
def run(inp: limnode.InDataTuple, out: limnode.OutDataTuple, par: limnode.UserParTuple, ctx: limnode.RunContext) -> None:
   global imgcache, fitted, basic
   if ctx.finalCall:
       if not fitted:
           stk = numpy.stack(imgcache, axis=0)
           basic.fit(stk)
           imgcache = []
           fitted = True
       out[0].data[0, :, :, 0] = basic.transform(inp[0].data[0, :, :, 0])[0]
   else:
       imgcache.append(numpy.copy(inp[0].data[0, :, :, 0]))
       fitted = False

# child process initialization (when outproc is set)
if __name__ == '__main__':
    from limnode import print
    limnode.child_main(run, output, build)
  1. Imports NumPy and basicpy from the BaSiC package [lines 2, 3].
  2. Initializes global variables for two pass processing [lines 5-7].
  3. Sets up two pass progam [line 15].
  4. Enables using global variables in run [line 19].
  5. Calculates shading correction (only once) and applies to every frame in second pass[lines 21-26].
  6. Collects data in first pass [lines 27-29].

Results

Matplotlib for data visualization

Matplotlib comes with NIS-Express.

This example will built on and modify the Object counting example.

Open the

  1. 02_count_in_t.nd2 time-lase nd2 image and
  2. 02_object_count_fluo.ga3 recipe.

Replace both graph nodes with a Python node. Both having Table as single input and single output.

Paste the code before connecting the output.

the recipe
Python nodes replacing standard GA3 graphs

Object count vs. time

  1. In the output() function [line 37] make the table result and provide it with input parameter (for taking the accumulation from the inp[0]),
  2. define the draw_graph() function [line 19] that actually draws the graph giving it the x, y data and background and foreground colors,
  3. call the draw_graph function twice for generating light and dark graphs [lines 48, 49] and finally
  4. call the withMplImage() method [line 52] to render the matplotlib figure into an image.

The withMplImage() takes following arguments:

  1. a tuple with ND loop indexes of the current frame or accumulated loop (ctx.inpParameterCoordinates[i] where i is the index of the input parameter is a good starting point) and
  2. a single figure or pyplot object or tuple of two such objects for light and dark color scheme.
Use the figure object as in the example below instead of matplotlib.pyplot global instance which may be in unexpected state when the node run function is called.
GA3 Python editor
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# IMPORTANT: 'limnode' must be imported like this (not from nor as)
import limnode

import numpy as np
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.ticker import FuncFormatter
import datetime, io, json

savefig_kwargs: dict = {
    'format': 'png',
    'bbox_inches': 'tight',
    'pad_inches': 0.25
}

def seconds_to_hms(x, _):
    return str(datetime.timedelta(seconds=int(x)))

def draw_graph(x, y, bgcolor, fgcolor):
    fig = Figure(figsize=(12, 4), facecolor=bgcolor)
    ax = fig.add_subplot(1, 1, 1, facecolor=bgcolor)
    ax.plot(x, y, marker='o', color='#77aadd', linewidth=2)
    ax.set_title("Object Count vs Time", fontsize=14, color=fgcolor)
    ax.set_xlabel("Time (hh:mm:ss)", fontsize=12, color=fgcolor)
    ax.set_ylabel("Count", fontsize=12, color=fgcolor)
    ax.grid(True, alpha=0.6, color='gray')
    ax.tick_params(axis='both', colors=fgcolor)
    for label in ax.get_xticklabels() + ax.get_yticklabels():
        label.set_color(fgcolor)
    for spine in ax.spines.values():
        spine.set_color(fgcolor)
    ax.xaxis.set_major_formatter(FuncFormatter(seconds_to_hms))
    return fig

# defines output parameter properties
def output(inp: limnode.InDefTuple, out: limnode.OutDefTuple, par: limnode.UserParTuple) -> None:
    out[0].makeResult("LineChart", inp[0])

# return Program for dimension reduction or two-pass processing
def build(par: limnode.UserParTuple, loops: limnode.LoopDefs) -> limnode.Program|None:
    return None

# called for each frame/volume
def run(inp: limnode.InDataTuple, out: limnode.OutDataTuple, par: limnode.UserParTuple, ctx: limnode.RunContext) -> None:
    Time = inp[0].colArray("Time")
    ObjectCount = inp[0].colArray("ObjectCount")

    canvasLight = draw_graph(Time, ObjectCount, '#fcfcfc', '#000000')
    canvasDark = draw_graph(Time, ObjectCount, '#444444', '#f0f0f0')


    out[0].withMplImage(ctx.inpParameterCoordinates[0], (canvasLight, canvasDark), savefig_kwargs=savefig_kwargs, iconres="line_common")

# child process initialization (when outproc is set)
if __name__ == '__main__':
    from limnode import print
    limnode.child_main(run, output, build)

The above python code produces following line chart.

Linechart light
Line chart by matplotlib in the light scheme
Linechart dark
Line chart by matplotlib in the dark scheme

Histogram of object sizes per frame

Similarly with histogram:

GA3 Python editor
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# IMPORTANT: 'limnode' must be imported like this (not from nor as)
import limnode

import numpy as np
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.ticker import FuncFormatter
import io, json

savefig_kwargs: dict = {
    'format': 'png',
    'bbox_inches': 'tight',
    'pad_inches': 0.25
}

def draw_graph(data, bgcolor, fgcolor):
    fig = Figure(figsize=(12, 4), facecolor=bgcolor)
    ax = fig.add_subplot(1, 1, 1, facecolor=bgcolor)
    ax.hist(data, bins=30, color='#77aadd', edgecolor='#77aadd22')
    ax.set_title("Histogram of Object Sizes", fontsize=14, color=fgcolor)
    ax.set_xlabel("Size (equivalent diameter)", fontsize=12, color=fgcolor)
    ax.set_ylabel("Frequency", fontsize=12, color=fgcolor)
    #ax.grid(True, alpha=0.6, color='gray')
    ax.tick_params(axis='y', colors=fgcolor)
    for label in ax.get_xticklabels() + ax.get_yticklabels():
        label.set_color(fgcolor)
    for spine in ax.spines.values():
        spine.set_color(fgcolor)
    return fig

# defines output parameter properties
def output(inp: limnode.InDefTuple, out: limnode.OutDefTuple, par: limnode.UserParTuple) -> None:
    out[0].makeResult("Histogram", inp[0])

# return Program for dimension reduction or two-pass processing
def build(par: limnode.UserParTuple, loops: limnode.LoopDefs) -> limnode.Program|None:
    return None

# called for each frame/volume
def run(inp: limnode.InDataTuple, out: limnode.OutDataTuple, par: limnode.UserParTuple, ctx: limnode.RunContext) -> None:
    ObjectSizes = inp[0].colArray("EqDiameter")

    canvasLight = draw_graph(ObjectSizes, '#fcfcfc', '#000000')
    canvasDark = draw_graph(ObjectSizes, '#444444', '#f0f0f0')

    out[0].withMplImage(ctx.inpParameterCoordinates[0], (canvasLight, canvasDark), savefig_kwargs=savefig_kwargs, iconres="histo_common")

# child process initialization (when outproc is set)
if __name__ == '__main__':
    from limnode import print
    limnode.child_main(run, output, build)

The above python code produces following histogram.

Histogram
Histogram by matplotlib

Conclusion

These two examples are not the fanciest plots. The lack interactivity as the native GA3 plots because they are rendered into a static image (or two: light and dark). However based on the aggregation of the inputs and loop coordinates the histogram will change as the current frame changes and linechart of counts vs. time will not as there is only one plot of aggregated counts.

Omnipose for bacteria segmentation

Omnipose is a pupular general image segmentation package that builds on Cellpose.

Setup

In the GA3 node add

  • one channel input,
  • one binary output,
  • one channel output and
  • set the Execution mode to “Managed environment”
  • insert the environment from the Environment section.
  • insert the code from Code section.
  • connect the input pin to the Channels output to be processed.
  • connect the binary output pin to the SavePictures node.
  • connect the channel output pin to the SaveBinaries node.

GA3 nodes

Environment

Edit the evironment and insert following definition:

channels:
    - conda-forge
dependencies:
    - python=3.10.12
    - pip
    - pip:
        - omnipose==1.1.4
channels:
    - conda-forge
dependencies:
    - python=3.10.12
    - pip
    - pip:
        - --index-url https://pypi.org/simple
        - --extra-index-url https://download.pytorch.org/whl/cu126
        - omnipose==1.1.4
        - torch==2.9.1
        - torchvision==0.24.1
        - torchaudio==2.9.1

Then install it by clicking the “Install managed environment” icon. Be patient it takes time.

The environment was created and tested in the January, 2026.

Consult with LLMs (e.g. ChatGPT) in case the versions do not work anymore.

Managed environment
Omnipose: Managed environment

Code

Insert the code as follows:

GA3 Python editor
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# IMPORTANT: 'limnode' must be imported like this (not from nor as)
import limnode

model = None
model_name = None

# defines output parameter properties
def output(inp: limnode.InDefTuple, out: limnode.OutDefTuple, par: limnode.UserParTuple) -> None:
    out[0].makeNew("Mask", (0, 255, 0)).makeUInt8()
    out[1].makeNewRgb("Flow").makeUInt8()

# return Program for dimension reduction or two-pass processing
def build(par: limnode.UserParTuple, loops: limnode.LoopDefs) -> limnode.Program|None:
    return None

# called for each frame/volume
def run(inp: limnode.InDataTuple, out: limnode.OutDataTuple, par: limnode.UserParTuple, ctx: limnode.RunContext) -> None:
    import numpy as np
    from omnipose.gpu import use_gpu
    from omnipose.utils import normalize99
    from cellpose_omni import models

    img = inp[0].data[0, :, :, 0]
    img = normalize99(img)

    global model, model_name
    new_model_name = 'bact_fluor_omni'
    if model is None or model_name != new_model_name:
        model_name = new_model_name
        gpu_device, is_gpu = use_gpu()
        if is_gpu:
            model = models.CellposeModel(gpu=(gpu_device, is_gpu), model_type=model_name)
        else:
            model = models.CellposeModel(model_type=model_name)

    params = {
        'channels':None,
        'rescale': None,
        'mask_threshold': -2,
        'flow_threshold': 0,
        'transparency': False,
        'omni': True,
        'cluster': True,
        'resample': True,
        'verbose': False,
        'tile': False,
        'niter': None,
        'augment': False,
        'affinity_seg': True
    }

    mask, flow, style = model.eval(img,**params)
    mask = limnode.separateLabeledImage(mask)
    out[0].data[0,:,:,0] = mask
    out[1].data[0,:,:,:] = np.where(mask[..., None], flow[0][:, :, 0:3], 0)[...,[2,1,0]]

# child process initialization (when outproc is set)
if __name__ == '__main__':
    from limnode import print
    limnode.child_main(run, output, build)

The code:

  1. Initializes the global variable model so the model is created only once. [line 4]
  2. Initializes the global variable model_name to track the currently selected model type. [line 5]
  3. Defines a binary output named “Mask” and sets its display color to green. [line 8]
  4. Defines a channel output named “Flow” and sets its format to 8-bit RGB. [line 9]
  5. Checks whether a GPU device is enabled [line 30]
  6. Creates the CPU model. [line 32]
  7. Creates the GPU model. [line 34]
  8. Runs the model on the current frame using the specified parameters. [line 52]
  9. Splits the labeled mask into separate binary objects. [line 53]
  10. Writes the resulting binary mask to the binary output [line 54]
  11. Applies the binary mask to the flow image, converts RGB to BGR, and writes the result to the channel output. [line 55]

Results

Segmented result
Omnipose: Binary Mask & RGB Flow

Export table to CSV

This workflow demonstrates a simple table export to CSV with customizable export options using an underlying pandas DataFrame (see pandas.pydata.org). It also shows how to add parameters for User Mode configuration.

Setup

Open the file that needs to be segmented.

In NIS Express GA3 Editor:

  • Add Python node and open its settings
    • Add table input
    • Insert code from Code section.
  • Connect the input pin to the Table output to be exported.

Code

GA3 Python Editor
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# IMPORTANT: 'limnode' must be imported like this (not from nor as)
import limnode
from pathlib import Path

# defines output parameter properties
def output(inp: limnode.InDefTuple, out: limnode.OutDefTuple, par: limnode.UserParTuple) -> None:
    pass

# return Program for dimension reduction or two-pass processing
def build(par: limnode.UserParTuple, loops: limnode.LoopDefs) -> limnode.Program|None:
    return None

# called for each frame/volume
def run(inp: limnode.InDataTuple, out: limnode.OutDataTuple, par: limnode.UserParTuple, ctx: limnode.RunContext) -> None:
    par_output = par[0] if len(par) > 0 else ""
    par_sep = par[1] if len(par) > 1 else ";"
    par_decimal = par[2] if len(par) > 2 else "."

    img_p = Path(ctx.outFilename)
    img_stem = img_p.stem
    img_dir = img_p.parent

    p = Path(par_output)
    if not p.is_absolute():
        p = img_dir / p
    if not p.suffix:
        p = p / f"{img_stem}.csv"
    p = p.resolve(strict=False)

    output_stem = p.stem if p.stem else img_stem
    output_suffix = p.suffix if p.suffix else ".csv"
    output_dir = p.parent

    output_dir.mkdir(parents=True, exist_ok=True)

    coords = ctx.inpParameterCoordinates[0]
    file_idxs = ("_" + "_".join(map(str, coords))) if coords else ""
    file_path = output_dir / f"{output_stem}{file_idxs}{output_suffix}"

    inp[0].data.pandasDataFrame().to_csv(file_path, index=False, sep=par_sep, decimal=par_decimal)

# child process initialization (when outproc is set)
if __name__ == '__main__':
    from limnode import print
    limnode.child_main(run, output, build)

The code:

  1. Imports Path from pathlib [line 3]
  2. Reads GUI parameters [lines 7-9]
  3. Extracts output image filepath info for use as default [lines 19-21]
  4. Resolves output path, adds default CSV name if missing, and normalizes it [lines 23-28]
  5. Extracts final filename, extension, and target directory [lines 30-32]
  6. Creates the output directory if it does not exist [line 34]
  7. Appends loop indices to filename to ensure unique files per iteration. [lines 36-38]
  8. Exports the input table to CSV with selected separator and decimal format [line 40]

User parameters

  1. Open the User parameters in the top toolbar of the node.
  2. In the opened window:
    • fill the description: Select options for CSV export
    • add parameters
      1. Output
        • Type: str
        • Definition: {"control":"Path","mode":"save","type":"file"}
      2. Delimiter
        • Type: str
        • Initial value: ;
        • Definition: {"control":"Selection","list":[{"text":"Semicolon ';'","value":";"},{"text":"Comma ','","value":","},{"text":"Tab '\\t'","value":"\t"}]}
      3. Decimal separator
        • Type: str
        • Initial value: .
        • Definition: {"control":"Selection","list":[{"text":"Dot '.'","value":"."},{"text":"Comma ','","value":","}]}

  1. Close the User parameters window
  2. Switch to the User mode in the top toolbar of the node.
  3. In the newly opened window click “Skip”
  4. Window should switch to User mode.
  5. If needed, return back by clicking Developer mode.

Results

CSV files are exported to the selected output directory, and export options can be easily modified via the custom GUI.