Stjörnhorn v0.3.0

A node-based image- and video-processing playground for the desktop.
beltoforion.de

Overview

Stjörnhorn is a desktop application for building image- and video-processing workflows in a node-based visual editor. Drop sources, filters, and sinks onto a canvas, wire them up, press Run, and watch the output update in the built-in viewer.

Typical uses:

  • Experiment with image-processing operations (dithering, thresholding, normalisation, scaling, channel splitting, …) without writing code.
  • Compose filters into reusable flows and save them to disk.
  • Batch-convert and composite images by wiring up file sources and sinks.
  • Drive parameters from numeric streams to animate values across a video — slide an overlay, ramp a threshold, fade in a mask.

Installation

Prerequisite: Python 3.10 or newer.

pip install -r requirements.txt

Or install in editable mode together with the stjornhorn console entry point:

pip install -e .

Pre-built binaries (Windows zip, Linux AppImage) are also attached to each tagged GitHub release — no Python toolchain required for end users.

Running

python src/main.py

Optional command-line arguments:

FlagDescription
--no-splash Skip the startup splash screen.
--flow FILE Load a flow at startup and open it directly in the editor. Accepts a full path to a .flowjs file or a bare flow name (looked up in flow/, with or without the .flowjs extension).

Unrecognised flags are forwarded to Qt — useful for -style, -platform, etc.

Start page

Stjörnhorn start page
The start page is the launch pad for working with flows.

The start page is the landing screen when the app opens. From here you create a new flow or pick up where you left off with an existing one.

  • Name input — type a name for a new flow. Names may contain ASCII letters, digits, and the characters _ # + -. The input also sets the filename stem that Save will use later (e.g. the name dither_lab saves to flow/dither_lab.flowjs).
  • Create — opens the node editor with a fresh empty flow whose name matches the input. Disabled until the input contains a valid name; pressing Enter in the input triggers it.
  • Open (toolbar, top) — launches a file dialog to load any .flowjs file from disk. The dialog starts in the app's flow/ directory but you can browse anywhere.
  • Recent Flows — a grid of tiles for flows you have recently created, opened, or saved. Click a tile to open that flow. Each tile shows the flow's name; hover to see the full path. The grid reads "No recent flows" until you have used one.

Node editor

Stjörnhorn node editor
Palette on the left, canvas in the middle, output preview docked below.

The node editor is where flows are built and run. A flow is a graph of nodes — sources produce images, filters transform them, sinks consume them — connected by typed ports. The editor gives you a palette, a canvas to wire nodes together, an output preview, and a toolbar to drive the flow.

Layout

  • Node List (dockable, left) — the palette of every registered node, grouped into a tree by section (Sources, Sinks, Output, Color Spaces, Transform, Processing, Temporal, Math, Composit, Debug, …). Each section is collapsible; the dock has expand-all / collapse-all buttons. A search box at the top filters live and auto-expands matching groups so leaves never hide behind a closed section. Drag an entry onto the canvas to instantiate it. Toggle the dock via the View menu; it can be floated, re-docked, or closed.
  • Canvas (centre) — the flow graph. Each node shows its title, output sockets stacked at the top, input sockets with inline parameter widgets at the bottom (Blender shader-editor layout). A small × in the top-right of a node deletes it; a diagonal grip in the bottom-right resizes the node — drag wider and every inline widget grows along with the body. Scroll to zoom; middle-mouse-drag to pan. Dropping a node from the palette places it at the cursor.
  • Status bar (bottom) — shows the last successful / informational message, such as "Ran at 14:23:55" or "Saved to flow/x.flowjs". Errors pop up in a floating red banner at the top right instead, so long multi-line messages stay readable.

Connecting nodes

  • Drag from an output port (top of a node) to an input port (bottom of another). The connection is only accepted if the port types are compatible — e.g. an Image (grey) output may feed an input that accepts greyscale.
  • Drag an existing link off either end to remove it.
  • One output can drive many inputs; each input accepts exactly one upstream.
  • Unconnected port circles are filled black; once connected, the dot fills with the type colour.

Port legend

The visual language of the ports is documented in the Node Documentation dock: when no node is selected, the dock shows a hint line followed by the port-type and port-role legend. Pick a node in the palette or on the canvas and the dock switches to that node's documentation; click empty canvas to clear the selection and the legend comes back.

The legend covers two axes:

  • Port types — each data type has its own colour. Connections are accepted only when the output port's type and the input port's accepted types overlap. An unconnected port shows the colour as a thin ring around an empty centre; a connected port fills with the colour.
  • Port roles — the ring style tells you how the port behaves:
    • Required input — solid ring. The node waits for data on this port before it can fire.
    • Optional input — thin solid ring. The node fires without waiting for this port. Leave it dangling and the node uses its default; connect it and it counts as required.
    • Latched input — dashed ring. The port keeps its last received value across frames. Useful for pairing a one-shot source (an image, a CSV) with a streaming clock — see Held inputs.
    • Output — small triangular glyph next to the dot. One output can drive any number of inputs; each input can be driven by at most one output.

Toolbar — Flow section

  • Run — execute the flow once. Sources push data through the graph to the sinks. The status bar updates with the run time; any exception shows up in the error banner.
  • Save — write the current flow to flow/<name>.flowjs, where <name> is the flow's current name.
  • Save As… — write the current flow to a path you choose. The stem of the chosen filename becomes the flow's new name (which is then used by future Save clicks).
  • Open — load another .flowjs file, replacing the current flow.
  • Clear — remove every node and connection from the canvas. Asks for confirmation.

Toolbar — View section

  • Fit — zoom and scroll so the whole graph fits the viewport.
  • 1:1 — reset the view transform to 100% zoom.
  • V-Stack — align two or more selected nodes on a shared X axis and stack them top-to-bottom (preserves their current vertical order). Disabled until ≥ 2 nodes are selected.
  • H-Stack — align two or more selected nodes on a shared Y axis and arrange them left-to-right (preserves their current horizontal order). Disabled until ≥ 2 nodes are selected.
Re-runs are explicit. Editing a parameter no longer auto-triggers a run — every re-run goes through the Run button. The previous 300 ms debounced auto-run was removed in v0.1.42 to avoid spurious decode work on video flows.

Building a flow

  1. From the start page, type a name for the flow and press Create.
  2. Drag a source from the palette (e.g. Image Source) onto the canvas. Click the path widget on the node body to pick an input file.
  3. Drag one or more filters onto the canvas and wire the source's output to the filter's input.
  4. Drag a Display node onto the canvas and wire it after the last filter — its inline preview is the result, no sink required.
  5. Press Run. The status bar shows the run time; the Display node renders the live frame inside its body.
  6. Tweak parameters and press Run again. To persist work, press Save — the flow is written to flow/<name>.flowjs.

Adding an output sink

To save the result to disk, add a File Sink (single image) or Video Sink (encode a stream) downstream of your filters in addition to (or instead of) Display. The eye button next to a File Sink's path opens the written file in the OS image viewer.

Flow files

Flows are JSON files with the .flowjs extension (just JSON — the extension makes them easy to associate with the app on the desktop). The format finalised in v0.1.35; on disk every node carries a port_defaults map of the literal default for each editable port. Older flows written with the legacy params key still load — the loader reads it as a fallback.

Saved flows store paths under the app's input/ and output/ folders as relative paths, so a flow stays portable across machines that share the same input layout.

Execution model

Stjörnhorn is push-based. Sources produce frames and push them downstream; each node fires as soon as every input it needs has data. There is no central scheduler walking the graph and no buffered queues between nodes — a frame moves from source to sink in a single chain of calls.

A linear flow

The simplest flow has one source, a few filters and a sink in a straight line:

Image Source Grayscale Invert File Sink
Image Source → Grayscale → Invert → File Sink. Each frame walks the chain in order.

Many inputs, one trigger

A node with several inputs (e.g. Mosaic, Masked Blend, RGBA Join) only fires once every required input has received data for the current frame:

Image Source A Image Source B Gradient Source Masked Blend Display
Masked Blend fires once per frame, after all three inputs (base, overlay, mask) have arrived.

One source, many consumers

An output can drive any number of inputs (fan-out). Each downstream consumer gets the same frame and runs independently:

Video Source Grayscale Gaussian Blur Invert Mosaic
Fan-out from one Video Source into three filters, recombined by Mosaic.

Streaming clocks and held inputs

When a one-shot source (an image, a CSV) feeds into a node alongside a streaming source (a Range Source counter, a Video Source), the one-shot input can be marked held — it remembers its single value and reuses it across every tick of the clock:

Image Source Range Source Repeat File Sink held clock
One image, many writes. The dashed wire is the held input — the same image fires once per tick, with the counter value stamped onto each output frame so File Sink's $value$ template writes frame_001.png, frame_002.png, …

The flow runs on a background thread so the UI stays responsive; the currently-executing node is highlighted live in the editor as the data marches through the graph.

Node anatomy

A node is a coloured box on the canvas with three kinds of slots: outputs at the top, inputs at the bottom, and parameters between them. Outputs produce values, inputs consume values, parameters configure how the node behaves each frame. Connections are made by dragging from an output socket on one node to an input socket on another.

Three kinds of node

  • Sources have outputs but no inputs. They feed data into the flow — load an image, decode a video file, walk a directory, generate a numeric ramp.
  • Filters have both inputs and outputs. They transform data — blur a frame, project a dataset, plot a curve, blend two images.
  • Sinks have inputs but no outputs. They consume data as a side effect — write a file, encode a video.

Each kind is colour-coded with a coloured stripe on the left of its node body in the editor.

Each input and output port carries a colour for its data type and a ring style for its role (required, optional, latched). The full visual key — embedded in the Node Documentation dock whenever no node is selected — is described in Node editor ▸ Port legend.

Parameters

Each parameter on a node is rendered as an inline widget on the node body — a spin box, slider, checkbox, drop-down or path picker — and comes in one of two flavours:

  • Port parameters sit on an input row with a socket dot next to the widget. Type a value into the widget to use it as a literal; or wire any compatible source into the socket and the streamed value drives the parameter per frame. When the port is driven, the inline widget is greyed out — the upstream wins until you disconnect it. See Param-as-port.
  • Constant parameters sit between the output and input rows with no socket dot, rendered with an italic caption. They configure the node once and are not drivable from upstream — file paths on sources, the codec on a video sink, the layout descriptor on Mosaic.

Reading the node body

Reading a node from top to bottom: name and the small × close button at the top, then output sockets (with the type colour and the output glyph), then any constant parameters in italic, then input sockets with their inline parameter widgets at the bottom. A diagonal grip in the bottom-right resizes the node — drag wider and every inline widget grows along with the body.

Data types

Every connection in a flow carries one of nine data types. The legend on the canvas shows the colour for each. A connection is accepted only when the output's type matches one of the input's accepted types.

TypeCarriesUsed for
Image A colour frame (BGR). Photos, video frames, colour output of filters.
Image (grey) A single-channel image. Masks, FFT magnitudes, NCC scores, dither output, individual colour channels.
Scalar A single numeric value. Counters, ramps, expression results — anything that drives a numeric parameter.
Matrix A 2-D numerical array of arbitrary type. Complex FFT spectra, transformation matrices, intermediate analytics that aren't images.
Dataset Labeled tabular data (rows × named columns). Seismic traces, CV curves, I-V sweeps, spectra — anything you'd plot with a plotter or feed through CSV.
Bool True / false. Drives boolean parameters.
String Text. Drives text-valued parameters (notification messages, column-name lists).
Enum A selection from a fixed list. Interpolation methods, dither algorithms, video codecs.
Path A file or folder path. Drives file-path parameters.

Many filters accept either Image or Image (grey) on the same input — Median, Scale, Shift, Rotate, Flip, Invert, Normalize, etc. — and emit whichever they received, preserving the colour space.

Frame metadata

Each value travelling between nodes carries a small bag of side metadata along with the payload. You don't usually see it, but two surfaces let you read it: the Meta Inspector debug node prints every key for the frame it sees, and the File Sink / Video Sink filename templates can reference any metadata key as $key$ to build per-frame output paths.

Some keys are stamped automatically — you don't need to wire anything up:

KeyStamped byMeaning
frame_index Every output port Per-output frame counter, starting at 0 on each run.
source_path Image Source, CSV Source, Directory Source Path the frame was loaded from. Filename templates also expand $source_stem$, $source_name$ and $source_ext$ from this.
(scalar input names) Any node with a Scalar input port The current value of every Scalar input on the emitting node is stamped under its port name. So if a filter has a Scalar input called tick, every frame it emits carries tick in its metadata, and a downstream sink can use $tick$ in its filename template.
window_start / window_end Sliding Window Row indices of the current window. Plot Series reads these to draw a moving band overlay across panels.

Dataset-side metadata

Dataset payloads also carry a parallel set of named attributes alongside the data table itself. Source nodes and analytic filters use these conventions; visualisation nodes read them when present.

AttributeSet / read byMeaning
source_path CSV Source sets it; plotters read it. The CSV file the dataset was loaded from.
units Directional Projection sets it; Plot XY, Plot Series and Hodogram read it. A per-column unit string used as an axis label suffix.
sample_rate Carried forward through every dataset filter. Sample rate in Hz. Set step = 1 / sample_rate on Add Index Column or Plot Series for a seconds axis.
thetas_rad Directional Projection sets it; Polar Heatmap reads it. The radial angle each column corresponds to, so polar visualisers don't have to parse column names.

See the File Sink entry for the full filename template syntax, including width-padding ($frame_index:4$ produces 0001, 0002, …).

Param-as-port

Every numeric, boolean, enum, string and path parameter on a node is also a port. Type a value into the inline widget to use it as a literal; or wire any source of the matching type into the port's socket and the streamed value drives the parameter once per frame.

While the port is connected, the inline widget is greyed out — the upstream wins. Disconnect the wire and the widget becomes editable again, retaining whatever value you last typed.

Typical use: connect a Range Source to the Overlay node's angle port to animate a rotation over a video, or to the xpos port to slide an overlay across the frame.

Held inputs

Some inputs keep the last value they received and reuse it across later frames. They render with a dashed ring around the socket dot.

The pattern is "one piece of reference data plus a streaming clock". Repeat uses it on its data input so a single still image or CSV survives across every tick of its tick clock. Sliding Window uses it on its dataset input so a one-shot dataset stays alive while a streaming counter walks the window across it.

A held input does not by itself end the run when its source finishes — only the streaming (non-held) inputs drive the end of the flow.

Skipping nodes

Right-click a filter and choose Skip to bypass it without removing the connections: the node's input is forwarded straight to its output, as if the node were replaced by a wire. Useful for A/B-comparing a flow with and without a filter.

Skip is only available when at least one input and one output of the node share a compatible type. Sources (no inputs) and sinks (no outputs) cannot be skipped.

Sources

Sources have outputs only — they feed data into the flow. Reactive sources emit once per run and re-fire automatically when any parameter changes, so still images and constants update live as you tweak knobs. Streaming sources push one frame at a time and only run when you press Run.

Image Source

Reads a single still image from disk and pushes it into the flow. Reactive: any parameter change re-runs the flow. The path the image came from is stamped on each frame's metadata under source_path, so downstream sinks can use $source_stem$ in their filename templates.

Outputs

  • imageImage. The decoded picture.

Parameters

  • file_path — path to a JPEG, PNG, WebP or CR2 (RAW) file. Paths inside the app's input folder are stored relative to it so saved flows port across machines.

Video Source

Decodes frames from a video file and streams them through the graph. Not reactive — runs only when you press Run, to avoid restarting long decodes on every keystroke. Frame count is shown as a badge in the node header once the file has been probed.

Outputs

  • imageImage. One BGR frame at a time.

Parameters

  • file_path — path to an MP4, AVI, MOV or MKV file.
  • max_num_frames — cap on the number of frames decoded. -1 means the whole stream.

Directory Source

Streams every readable image in a folder as a sequence of frames, sorted by file name. Useful as a "video out of a folder of stills" fixture. Each emitted frame carries the originating file's source_path in its metadata, so a downstream File Sink template can reference $source_stem$ to write per-input-image output paths.

Outputs

  • imageImage. One file per frame.

Parameters

  • directory — folder to walk.
  • include_subdirectories — when on, walks the folder recursively. When off, only the top level is read.

CSV Source

Reads a CSV file as a Dataset. Lines starting with # are skipped automatically, so files with a metadata header (seismic ASCII traces, gnuplot output, instrument logs) load without extra setup. Reactive: any parameter change re-runs the flow. The originating file is stamped both on the frame metadata and on the dataset's own attribute table under source_path.

Outputs

  • datasetDataset. Each CSV column becomes a column in the dataset.

Parameters

  • file_path — path to the CSV / TXT file.
  • has_header — when on, the first non-comment line names the columns; when off, columns are auto-named c0, c1, …
  • delimiter — column separator. Use \t to mean a tab character.

Gradient Source

Generates a single-channel greyscale gradient image. Use as a procedural mask for Masked Blend — tilt-shift, vignette, cross-fade — without shipping a hand-painted PNG. Reactive.

Outputs

  • imageImage (grey). Width × height greyscale image.

Parameters

  • width / height — output image size in pixels.
  • directionVertical, Horizontal or Radial (centre-outward).
  • modeSymmetric (0 in the middle, 255 at both ends — vignette, tilt-shift) or Linear (0 → 255, one-sided cross-fade). Ignored for radial direction.
  • band_width — width of the central plateau, 0..1. Higher values keep more of the image at the maximum.

Range Source

Emits a numeric range, one value per frame. Drives downstream parameters — animate an Overlay angle, a Math expression input, a counter for File Sink templating. Whole-number increments emit integers; fractional increments emit floats. Streaming.

Outputs

  • valueScalar. One number per frame.

Parameters

  • min_value / max_value — inclusive bounds of the emitted sequence.
  • increment — step between consecutive values. Must be positive.

Constant Value

Emits a single number. Reactive. Use as a fixed factor or offset feeding into Math, or to drive any numeric parameter from a single editable knob without spinning up a Range Source.

Outputs

  • valueScalar.

Parameters

  • value — the number to emit.

Output

Display

A pass-through node that shows each frame inline inside its own body via a live preview. Drop it anywhere in a flow to watch the data flow through; resize the node body to grow the preview. Accepts images, scalars and matrices — scalars and matrices render as text, so the inline preview is the result for those payload kinds. Image previews show a smoothed FPS read-out in the corner.

Inputs

  • imageImage, Image (grey), Scalar or Matrix.

Outputs

  • image — same payload, passed through unchanged.

Sinks

Sinks consume data as a side effect — writing a file, encoding a video — and do not propagate further. A flow that ends at a Display node runs fine without one.

File Sink

Writes each incoming frame to disk. The path is a filename template that expands placeholders against the frame's metadata, so a single sink can write a whole sequence of numbered files.

Inputs

  • imageImage or Image (grey).

Parameters

  • output_path — destination path. May contain $token$ placeholders that expand at write time:
    • $frame_index$ — the per-frame counter.
    • $source_stem$, $source_name$, $source_ext$ — the originating file (when the frame came from Image Source, Directory Source or CSV Source).
    • $flow_name$ — the saved name of the flow.
    • Any Scalar port value that an upstream filter stamped — see Frame metadata.
    Width syntax $tok:N$ zero-pads numeric values, e.g. frame_$frame_index:4$.png writes frame_0001.png, frame_0002.png, … With no placeholders the file is overwritten on every frame.

An eye button next to the path opens the most recently written file in the OS default image viewer.

Video Sink

Encodes incoming frames into a video file. The encoder opens on the first frame (dimensions are taken from the data), every later frame must match its shape, and the container is finalised when the upstream stream ends. Greyscale inputs are promoted to BGR automatically.

Inputs

  • imageImage or Image (grey).

Parameters

  • output_path — destination path. Filename tokens are resolved from the first frame's metadata (per-frame tokens like $frame_index$ aren't useful here — for per-frame paths, use File Sink instead).
  • fps — frame rate written into the container header.
  • codecMP4V (default) or XVID.

Color Spaces

Convert images between colour spaces or split them into per-channel greyscale planes for independent processing, then merge back.

Grayscale

Converts a colour image to single-channel greyscale.

Inputs

  • imageImage.

Outputs

  • imageImage (grey).

RGBA Split

Splits a colour image into its four channels. Plain (non-alpha) inputs produce a fully-opaque alpha plane so downstream nodes always see a well-defined alpha output.

Inputs

  • imageImage.

Outputs

  • B / G / R / AImage (grey), one per channel.

RGBA Join

Merges single-channel inputs into a colour image. The alpha input is optional: connect it for a BGRA output; leave it open for plain BGR. Pixel-exact inverse of RGBA Split.

Inputs

  • B / G / RImage (grey), required.
  • AImage (grey), optional. When connected, the output carries the alpha channel.

Outputs

  • imageImage.

HSV Split

Splits a colour image into its HSV components. Hue uses the full 0..255 range so HSV Join round-trips back exactly. BGRA inputs drop the alpha channel; HSV has no alpha.

Inputs

  • imageImage.

Outputs

  • H / S / VImage (grey), one per channel.

HSV Join

Inverse of HSV Split. Merges H, S, V planes back into a colour image.

Inputs

  • H / S / VImage (grey).

Outputs

  • imageImage.

HSL Split

Same as HSV Split, but using the HSL colour model.

Inputs

  • imageImage.

Outputs

  • H / S / LImage (grey).

HSL Join

Inverse of HSL Split.

Inputs

  • H / S / LImage (grey).

Outputs

  • imageImage.

Transform

Scale

Resizes the input by a percentage factor — 100 leaves it unchanged, 50 halves it, 200 doubles it.

Inputs / Outputs

  • imageImage or Image (grey); output type matches input.

Parameters

  • scale_percent — scale factor in percent.
  • interpolation — resampling method. Nearest is fast and pixelated; Linear, Cubic and Lanczos4 are progressively smoother and slower; Area is the right choice when downsampling.

Resize

Resizes the input to an explicit width and height using one of three layout strategies. Black is used for fills; for BGRA inputs that means transparent black.

Inputs / Outputs

  • imageImage or Image (grey); output type matches input.

Parameters

  • width / height — target dimensions in pixels.
  • methodScale stretches the source on each axis independently (aspect ratio not preserved); Crop or Fill centres the source at native scale on a target-sized canvas (overflow is cropped, uncovered area is black); Best Fit scales uniformly to the largest size that fits inside the target, then centres on a black canvas (letterbox / pillarbox).

Shift

Translates an image by integer pixel offsets. The canvas stays the same size; pixels that move out of frame are dropped, newly exposed areas are black.

Inputs / Outputs

  • imageImage or Image (grey).

Parameters

  • offset_x — horizontal shift in pixels. Positive moves right.
  • offset_y — vertical shift in pixels. Positive moves down.

Crop

Cuts a rectangular region out of the input. The rectangle is clamped to the input bounds, so the node always emits a positive-area image even if the requested rectangle extends beyond the source.

Inputs / Outputs

  • imageImage or Image (grey).

Parameters

  • x / y — top-left corner of the rectangle in input-pixel coordinates.
  • width / height — rectangle size in pixels.

Rotate

Rotates the image around its centre by an arbitrary angle. Positive angles are counter-clockwise.

Inputs / Outputs

  • imageImage or Image (grey).

Parameters

  • angle — rotation in degrees.
  • expand — when on, the output canvas grows so no pixel is clipped. When off, the canvas keeps the input dimensions and corners may fall outside.

Flip

Mirrors the image. Both is equivalent to a 180° rotation.

Inputs / Outputs

  • imageImage or Image (grey).

Parameters

  • modeHorizontal, Vertical or Both.

Processing

Adaptive Gaussian Threshold

Adaptive binary thresholding using a local Gaussian-weighted mean. Each output pixel is white or black depending on whether the source pixel was above or below the local mean of its neighbourhood, minus a constant. Useful for binarising photos with uneven lighting.

Inputs

  • imageImage or Image (grey). Colour inputs are reduced to greyscale first.

Outputs

  • imageImage (grey). Always single-channel binary.

Parameters

  • block_size — side length of the neighbourhood used to compute the local threshold, in pixels. Must be odd and ≥ 3.
  • c — constant subtracted from the local mean. Negative values bias toward white; positive toward black.

Dither

Reduces the image to two levels using a chosen dithering algorithm — Bayer ordered patterns, random noise, or various error-diffusion kernels. Colour inputs are auto-converted to greyscale; output is always greyscale.

Inputs

  • imageImage or Image (grey).

Outputs

  • imageImage (grey).

Parameters

  • method — one of Bayer 2 / 4 / 8 (ordered patterns; deterministic, no neighbour artefacts), Noise (random threshold), Floyd–Steinberg, Stucki, Atkinson, Burkes, Sierra, Diffusion X, Diffusion XY (error-diffusion kernels of varying spread).

Median

Square-kernel median blur. Effective at removing salt-and-pepper noise without smearing edges.

Inputs / Outputs

  • imageImage or Image (grey); output type matches input.

Parameters

  • size — kernel side length in pixels. Must be odd and ≥ 3. Larger kernels remove more noise at the cost of fine detail.

Gaussian Blur

Smooths the image with an isotropic Gaussian kernel.

Inputs / Outputs

  • imageImage or Image (grey).

Parameters

  • ksize — kernel side length in pixels. Must be odd and ≥ 3. Larger kernels blur more strongly and run more slowly.
  • sigma — standard deviation of the Gaussian. Set to 0 to derive it automatically from the kernel size.

Normalize

Histogram equalisation. Stretches the dynamic range so dim images use the full 0..255 scale. Colour inputs are equalised per channel.

Inputs / Outputs

  • imageImage or Image (grey); output type matches input.

Invert

Per-channel image inversion (255 − pixel). Type-preserving.

Inputs / Outputs

  • imageImage or Image (grey).

Apply Colormap

Colorises a greyscale image through a chosen lookup table and emits a colour image. Expects the input to already be tonemapped into 0..255; for HDR sources (raw FFT magnitudes, depth maps) apply log compression upstream.

Inputs

  • imageImage (grey).

Outputs

  • imageImage.

Parameters

  • colormap — perceptually-uniform palettes (Viridis, Plasma, Magma, Inferno), classic spectrogram palettes (Jet, Turbo) and utility palettes (Hot, Bone, Parula, Ocean, Cool).

NCC

Normalised cross-correlation template matching: searches for a small reference image inside the input and emits a score map showing how strongly each location matches.

Inputs

  • imageImage (grey). The picture to search.

Outputs

  • imageImage (grey). The score map.

Parameters

  • template — path to the reference image. Colour templates are reduced to greyscale on load.
  • retain_size — when on (default), the score map is padded to the input size and each response is centred on its corresponding template-centre pixel. When off, the raw response is emitted, smaller than the input.

Temporal

Temporal nodes accumulate state across frames of a video stream. The state is reset at the start of every run.

Frame Difference

Per-pixel absolute difference between the current frame and the previous one. The first frame in a stream produces a black output (no prior frame to diff against). A mid-stream resolution change resets the buffer rather than raising.

Inputs / Outputs

  • imageImage or Image (grey).

Temporal Mean

Rolling per-pixel arithmetic mean over the last few frames. Reduces additive Gaussian-style noise on a static scene at the cost of smearing motion. While the buffer fills, the mean of however many frames have arrived so far is emitted.

Inputs / Outputs

  • imageImage or Image (grey).

Parameters

  • window — number of recent frames averaged per output. Larger values denoise more aggressively but blur motion.

Temporal Median

Rolling per-pixel median over the last few frames. Strong against transient outliers — single-frame spikes, flicker, salt-and-pepper noise — without the smearing a mean would produce.

Inputs / Outputs

  • imageImage or Image (grey).

Parameters

  • window — number of recent frames whose per-pixel median is emitted.

Frequency

Frequency-domain operations on greyscale images and on datasets.

FFT 2D

Computes the 2-D discrete Fourier transform of a greyscale image. Pair with Inverse FFT 2D for a pixel-exact round trip; pair with Apply Colormap on the magnitude output for a coloured spectrum preview.

Inputs

  • imageImage (grey).

Outputs

  • spectrumMatrix. The complex spectrum, fftshifted so DC is at the centre. Wire into Inverse FFT 2D to round-trip.
  • magnitudeImage (grey). Log-scaled magnitude normalised to 0..255 for direct preview.

Inverse FFT 2D

The inverse of FFT 2D. Expects the fftshifted complex spectrum and emits a greyscale image.

Inputs

  • spectrumMatrix.

Outputs

  • imageImage (grey).

Spectrum

Per-column magnitude FFT of a dataset. Each column of the input becomes a column of the output, indexed by frequency in Hz. Feed the result to Plot Series for stacked spectra, or to Polar Heatmap when combined with Directional Projection.

Inputs / Outputs

  • datasetDataset.

Parameters

  • sample_rate — sample rate of the input in Hz; sets the frequency axis.
  • freq_max — clip the radial axis to this frequency. 0 keeps the full Nyquist range.
  • window — taper applied per column before the FFT. Hann suppresses spectral leakage on short windows; None uses the raw rectangular window.
  • db_scale — when on, emits 20·log₁₀|F| (dB); when off, raw magnitude.

Math

Math nodes operate on numeric streams. Combine them with Range Source and Constant Value to drive any numeric parameter on any node from a computed value.

Math

Evaluates a small expression over up to nine numeric inputs. The expression is plain Python-style arithmetic using the input names v_1, v_2, … v_9; min, max, abs; and a curated set of trigonometry and basic math functions (sin, cos, sqrt, log, …). The node body starts with one input row and grows by one as you wire up the previous tail.

Examples: v_1 + v_2, min(v_1, v_2), v_1 * sin(v_2).

Inputs

  • v_1v_9Scalar, all optional. Disconnected slots are simply absent from the expression's namespace.

Outputs

  • valueScalar. The expression result.

Parameters

  • expression — the formula to evaluate.

Clamp

Clamps a numeric input into a bounded range. If the upper bound is below the lower bound the two are swapped before clamping, so a transiently-inverted range during editing is tolerated.

Inputs / Outputs

  • valueScalar.

Parameters

  • min_value — lower bound.
  • max_value — upper bound.

Composit

Combine two or more images into a single output frame.

Mosaic

Variable-layout image composer. The layout is a tiny string: rows separated by ;, cells inside a row by ,, each cell the 1-based index of an image_i input. For example 1,2;3 places images 1 and 2 side-by-side on the top row and image 3 below.

Within a row, images are scaled aspect-preserving to the row's tallest input, then placed side-by-side. Rows are then scaled aspect-preserving to the widest row's width and stacked vertically. No black bars, no forced grid — aspect ratios are preserved everywhere. Mixed colour / greyscale inputs are promoted to colour so detail isn't lost.

Inputs

  • image_1image_9Image or Image (grey). Body grows as you wire them up.

Outputs

  • image — composed picture.

Parameters

  • layout — the layout descriptor string.

Overlay

Composites an overlay image onto a base. The overlay is optionally rotated and scaled, then blended so its centre lands at the configured position. Every numeric parameter is a port — wire a Range Source into angle for spinning, into xpos for sliding, into alpha for fading.

A BGRA overlay's alpha channel acts as a per-pixel mask; the alpha parameter is then a global multiplier on top. The output canvas matches the base; the output is colour if either input is colour.

Inputs

  • baseImage or Image (grey).
  • overlayImage or Image (grey).

Outputs

  • image — composited result.

Parameters

  • scale — overlay scale factor. 1.0 = unchanged. Strictly positive.
  • angle — overlay rotation in degrees, counter-clockwise around its centre. The bounding box is expanded so no pixels are lost.
  • xpos / ypos — base-image coordinates of the overlay's centre.
  • alpha — opacity, 0..1. 0 hides the overlay entirely, 1 makes it fully opaque.

Masked Blend

Per-pixel cross-fade of two images driven by a greyscale mask. Black mask emits the base, white mask emits the overlay, intermediate values cross-fade. Unlike Overlay's uniform alpha, here the mask is a full input — wire any greyscale producer in to drive it (a Gradient Source, an FFT magnitude, a threshold result).

The output is colour if either input is colour. The mask is reduced to a single channel and resized to match the base if its shape differs.

Inputs

  • baseImage or Image (grey).
  • overlayImage or Image (grey). Must share the base's H × W.
  • maskImage or Image (grey).

Outputs

  • image — blended result.

Data

Data nodes operate on Dataset values — the labeled tabular payload kind. Source datasets typically come from CSV Source; results feed into a visualisation node or back to disk.

Add Index Column

Prepend a synthetic numeric column to the input dataset at position 0, so it becomes the default X axis downstream. Useful when a renderer needs an explicit X axis but the dataset has no natural one — e.g. a one-column trace from CSV Source. Set step to 1 / sample_rate for a seconds axis.

Inputs / Outputs

  • datasetDataset.

Parameters

  • name — name of the new column.
  • start — value of the first row.
  • step — increment per row. Use 1.0 for sample numbers, 1 / sample_rate for seconds.

Join Datasets

Merges several datasets column-by-column into a single one. The body grows as you wire more inputs.

column_names renames the first column of each input before joining — required when several single-column sources all carry the same generic name (e.g. c0 from CSV Source). Empty keeps the original names, which must then be distinct.

Inputs

  • dataset_1dataset_9Dataset.

Outputs

  • dataset — joined dataset.

Parameters

  • column_names — comma-separated rename list, applied to the first column of each connected input.

Directional Projection

Projects a two-component vector signal onto a fan of directions. For each angle around the circle, emits one column equal to x · cos θ + y · sin θ — i.e. the component of the vector along that direction. The building block for directional analyses (polar spectra, polarisation sweeps, beam-forming on two-channel data).

Output columns are named in degrees (e.g. 5.0°) so a Plot Series reads naturally; the exact angles are also placed on the dataset's attribute table so Polar Heatmap can read them directly.

Inputs / Outputs

  • datasetDataset.

Parameters

  • x_column / y_column — names of the two component columns. Empty defaults to the first two columns.
  • n_angles — number of directions to emit, equally spaced around the full circle.

Sliding Window

Walks a sliding window across a dataset, driven by a numeric clock. Each tick of the clock re-emits two datasets: the current window slice, and the unmodified full dataset (with the current window bounds stamped on its metadata, so a downstream plotter can draw a moving band without a separate wire).

The dataset input is held — one upstream emit (e.g. one CSV) keeps it alive across all ticks of the clock.

Inputs

  • datasetDataset, latched. The data to walk.
  • window_indexScalar. The clock — typically a Range Source.

Outputs

  • dataset_windowedDataset. The current slice.
  • dataset_fullDataset. The unmodified input, with window_start / window_end stamped on its metadata.

Parameters

  • window_size — number of rows in each emitted slice. The window is clamped at the end of the dataset, so the last slice may be shorter.
  • step — number of rows between consecutive windows.

Visualization

Visualization nodes render datasets as images, suitable for chaining into Display, a Mosaic, or a File Sink.

Plot XY

Renders two columns of a dataset as an XY line plot. Axis labels come from the column names; if the dataset carries a units attribute it is added as a suffix to each axis label.

Inputs

  • datasetDataset.

Outputs

  • imageImage. The rendered plot.

Parameters

  • x_column / y_column — names of the columns to plot.
  • width / height — output canvas size in pixels.

Plot Series

Renders every column of the input dataset as an independent time-series trace stacked vertically with a shared time axis. The X axis is synthesised from a per-row step.

When the dataset carries window-bound metadata (typically stamped by Sliding Window), a moving band is overlaid spanning every panel — the highlighted window lines up across channels.

Inputs

  • datasetDataset.

Outputs

  • imageImage.

Parameters

  • step — increment per row on the X axis. Use 1 / sample_rate for a seconds axis.
  • y_columns — comma-separated list of columns to plot. Empty plots every column.
  • width / height — output canvas size.

Hodogram

Plots a two-channel signal as a time-coloured 2-D trajectory: one channel feeds the X axis, another feeds Y, and the line colour walks through a perceptual colormap so the time direction reads correctly. An optional fitted principal axis overlays the dominant direction. Useful for polarisation analysis on seismic and geophone data.

Inputs

  • xDataset. Provides the X channel.
  • yDataset. Provides the Y channel.

Outputs

  • imageImage.

Parameters

  • width / height — output canvas size.
  • principal_axis — when on, a fitted line overlays the dominant direction of motion.

Polar Heatmap

Renders a dataset of N directional channels as a polar heatmap. Each column corresponds to one direction; rows map to radius. Typically fed by Directional Projection or Spectrum. When the input carries the thetas_rad attribute, those angles drive the layout; otherwise degrees are parsed from the column names.

Inputs

  • datasetDataset.

Outputs

  • imageImage.

Parameters

  • width / height — output canvas size.
  • colormap — palette for the heatmap (Viridis, Plasma, Inferno, Magma, Turbo, Jet, Hot).

Streaming

Bridge between one-shot data and a streaming clock.

Repeat

Re-emits a held payload once per tick of a clock. Pairs a one-shot source (an image, a CSV) with a streaming clock so the held payload fires once per tick, with the clock value stamped onto each output frame's metadata. Downstream sinks can then use $tick$ in their filename templates without needing an extra input.

Inputs

  • data — any payload kind, latched. The reference data — typically a single image or dataset.
  • tickScalar. The clock.

Outputs

  • data — same payload as the input, re-emitted once per tick. Carries tick on its metadata.

UI

Pass-through nodes that produce a UI side-effect alongside the frame.

Delay

Sleeps for a configurable number of seconds between frames before forwarding each one. Useful as a slideshow knob, or to make per-frame status updates visible during development.

Inputs / Outputs

  • imageImage or Image (grey).

Parameters

  • delay_seconds — how long to sleep between frames. Set to 0 to disable pacing entirely.

Notify

Surfaces a status message in the floating banner. Info (blue) and Warning (amber) let the run continue; Error (red) aborts the run. The message is a port — wire any text source in to drive it per frame.

Inputs / Outputs

  • imageImage or Image (grey). Pass-through.

Parameters

  • message — text shown in the banner.
  • severityInfo, Warning, or Error.

Experimental

Effects-style nodes that aren't part of the standard pipeline but are interesting on their own.

Subpixel Mosaic

Renders a colour image as a stylised RGB sub-pixel mosaic: each source pixel is scattered across three mono-channel sub-pixels arranged like a physical display's shadow mask. The raw mosaic distorts the source aspect ratio; an option rescales it back.

Inputs

  • imageImage.

Outputs

  • imageImage or Image (grey) depending on parameters.

Parameters

  • keep_aspect — when on, the mosaic is resampled to match the source aspect ratio. When off, the raw mosaic is emitted (vertical stretch of 4/3).
  • output_grayscale — when on, drops the colour and emits the per-pixel sample intensity as a single-channel image.

Debug

Debug nodes are utilities for poking at the application itself — testing parameter wiring, simulating slow nodes, exercising the error banner. They live in their own palette section so they stay out of the way during normal use.

Debug Params

Exhaustive node exposing one parameter of every kind, so every widget can be rendered, edited, saved and loaded through a single node. Image is passed through unchanged.

Inputs / Outputs

  • imageImage or Image (grey).

Parameters

  • file_path — exercises the path widget.
  • count — exercises the integer spin box.
  • factor — exercises the float spin box.
  • label — exercises the line edit.
  • enabled — exercises the checkbox.
  • mode — exercises the combo box.

Throw Exception

Raises on every frame. Use it to confirm that the error banner pops up and that flows recover cleanly when a node fails mid-graph.

Inputs / Outputs

  • imageImage.

Meta Inspector

Surfaces each frame's metadata to an inline preview inside the node body. Accepts every payload kind so it can sit anywhere as a debug probe. Use it to inspect what source_path, frame_index, stamp keys etc. look like at a given point in your flow — handy when wiring up a filename template.

Inputs / Outputs

  • data — any payload kind.

Play Gate

Buffers frames until you click the inline Play button. Each click releases one frame downstream — the oldest in the queue, so you step through the stream in arrival order. The queue is unbounded, so it trades memory for honesty rather than silently dropping; keep the upstream stream short. Drains its buffer when toggled into skip mode.

Inputs / Outputs

  • data — any payload kind.

File locations

In dev mode (running python src/main.py from a clone), all files live next to the source tree. In a frozen bundle they split: read-only data sits inside the bundle (sys._MEIPASS), writable data moves to a per-user directory.

Purpose Dev mode Frozen (Linux) Frozen (Windows)
Saved flows flow/ ~/.local/share/Stjornhorn/flow/ %LOCALAPPDATA%\Stjornhorn\flow\
Sample inputs input/ ~/.local/share/Stjornhorn/input/ %LOCALAPPDATA%\Stjornhorn\input\
Default output dir output/ ~/.local/share/Stjornhorn/output/ %LOCALAPPDATA%\Stjornhorn\output\
Logs logs/image-inquest.log ~/.local/share/Stjornhorn/logs/… %LOCALAPPDATA%\Stjornhorn\logs\…
User-defined nodes ~/.image-inquest/user_nodes/

On first launch of a frozen bundle, the per-user directories are created and seeded from the bundled samples — only files that don't already exist are copied, so anything you save persists across launches.

User-defined nodes

Drop a Python module under ~/.image-inquest/user_nodes/ that defines a subclass of NodeBase, SourceNodeBase, or SinkNodeBase and Stjörnhorn picks it up at startup via the same AST-based registry scan it uses for built-in nodes. Declare your inputs as InputPort objects (with type, default, and widget metadata) and your outputs as OutputPort objects, implement process_impl(), and your node appears in the palette under whichever section=… string you passed to super().__init__.

Keyboard & mouse

ActionShortcut
Create the named flow (start page) Enter
Toggle full-screen preview (output inspector floated) F11
Pan the canvas Middle-mouse drag
Zoom the canvas Mouse wheel
Delete a node × button on the node, or Del while selected
Resize a node Drag the diagonal grip in the bottom-right of the node
Multi-select nodes Drag a rubber-band on empty canvas, or Shift+click

License

Stjörnhorn is released under the MIT License.