interactive-pipe 0.8.9
Library to create flexible interactive image processing pipelines and automatically add a graphical user interface without knowing anything about GUI coding!
Requires Python: >=3.7


<center>

| ![Interactive pipe](/static/interact-pipe-logo-horizontal-rgb.svg) |
|:--:|
|Quick setup `pip install interactive-pipe` |
| [Project website](https://balthazarneveu.github.io/interactive_pipe/) |
|[![Interactive pipe python package](https://github.com/balthazarneveu/interactive_pipe/actions/workflows/pytest.yaml/badge.svg)](https://github.com/balthazarneveu/interactive_pipe/actions/workflows/pytest.yaml) |

</center>

# Interactive-pipe code
### Concept


- Develop an algorithm while debugging visually with plots, while checking robustness & continuity to parameters change.
- Magically create a graphical interface to easily demonstrate a concept or simply tune your algorithm.

❤️ **You do not need to learn anything about making a graphical user interface (GUI)** ❤️


### Examples


| Science notebook | Toddler DIY Jukebox on a raspberry Pi |
|:-----: |:-----:|
| ![science notebook](/doc/images/scientific_notebook.jpg) |  ![jukebox](/doc/images/jukebox.jpg)
| Sliders are added automatically in your jupyter notebook. This works on Google Collab and the code takes about 40 lines of code. No Widgets, event handlers or matplotlib knowledge required.| Plays some music when you touch the icon. Caption added through the title mechanism. Music samples generated by prompting [MusicGen](https://huggingface.co/spaces/facebook/MusicGen) |
| [Demo notebook on collab](https://colab.research.google.com/drive/1AwHyjZH8MnzZqwsvbmxBoB15btuMIwtk?usp=sharing) | [jukebox_demo.py demo code](/demo/jukebox_demo.py)|


----------------------

### Local setup
```
git clone git@github.com:balthazarneveu/interactive_pipe.git
cd interactive-pipe
pip install -e ".[full]"
```

----------------------

### Who is this for? 
#### 🎓 Scientific education
- Demonstrate concepts by interacting with curves / images.
- Easy integration in Jupyter Notebooks (popular with Google Collab) 
#### 🎁 DIY hobbyist
- You can also use the declarative nature of interactive pipe to make a graphical interface in a few lines of codes. 
- For instance, it is possible to code a jukebox for a toddler on a RaspberryPi.
#### 📷 Engineering *(computer vision, image/signal processing)*
- While prototyping an algorithm or testing a neural network, you may be interested in making small experiments with visual checks. Instead of making a draft quick & dirty code that you'll never keep, you can use interactive pipe to show your team how your library works. A visual demo is always good, it shows that the algorithm is not buggy if anyone can play with it.
- Tune your algorithms with a graphical interface and save your parameters for later batch processing.
- Ready to batch under the hood, the processing engine can be ran without GUI (therefore allowing to use the same code for tuning & batch processing if needed).
- Do not spoil your production code with a huge amount of graphical interface code, keep your algorithms library untouched and simply decorate it.




----------------------

## 📜  Features

**Version 0.8.9**
- Modular multi-image processing filters
- Declarative: Easily make graphical user interface without having to learn anything about pyQt or matplotlib
- Support in jupyter notebooks
- Tuning sliders & check buttons  with a GUI
- Cache intermediate results in RAM for much faster processing
- `KeyboardControl` : no slider on UI but exactly the same internal mechanism, update on key press.
- Support Curve plots (2D signals).
- Gradio backend (+allows sharing with others). 
- Audio support in Gradio (live audio or display several players by returning 1D numpy arrays)
- Circular sliders for Qt Backend
- Text prompt (`free_text=("Hello world!", None),`)
- TimeControl (possibility to play/pause time using an incrementing timer)
- 🆕 **Context API**: Direct access to shared context across filters via `get_context()`, `context`, `layout`, `audio` 
- 🆕 MIT License
- 🆕 Panel **Panel System**: group the sliders in defined panels. allows fine control on GUI layout.
- 🆕 Support Table outputs



#### ⌨️   Keyboard shortcuts
Shortcuts while using the GUI (QT & matplotlib backends)

- `F1` to show the help shortcuts in the terminal
- `F11` toggle fullscreen mode
- `W` to write full resolution image to disk
- `R` to reset parameters
- `I` to print parameters dictionary in the command line
- `E` to export parameters dictionary to a yaml file
- `O` to import parameters dictionary from a yaml file (sliders will update)
- `G` to export a pipeline diagram for your interactive pipe (requires graphviz)



# Status
- supported backends 
    - ✅ `gui='qt'` pyQt/pySide 
    - ✅ `gui='mpl'` matplotlib
    - ✅ `gui='nb'`  ipywidget for jupyter notebooks
    - 🧪 `gui='gradio'` gradio wrapping (+use `share_gradio_app=True` to share your app with others)
- tested platforms
    - ✅ Linux (Ubuntu / KDE Neon)
    - ✅ RapsberryPi
    - ✅ On google collab (use `gui='nb'`)



| ⭐ | *PyQt / PySide* |  *Matplotlib*   | *Jupyter notebooks including Google collab*   | *Gradio* | 
|:-----: |:-----:|:------:|:----: |:----: |
| Backend name | `qt`  | `mpl`  | `nb`| `gradio` |
| Preview | ![qt backend](/doc/images/qt_backend.jpg)   | ![mpl backend](/doc/images/mpl_backend.jpg)    |  ![nb backend](/doc/images/notebook_backend.jpg)  | ![mpl backend](/doc/images/gradio_backend.jpg)| 
| Plot curves | ✅ | ✅ |   ✅ | ✅|
| Change layout | ✅ | ✅ |   ✅ | ➖ |
| Keyboard shortcuts / fullscreen| ✅ | ✅ |  ➖ | ➖ |
| Audio support | ✅ | ➖ |  ➖ | ✅ |
| Image buttons| ✅ | ➖ |  ➖ | ➖ | 
| Circular slider| ✅ | ➖ |  ➖ | ➖ |
| Collapsible Panels | ✅ | ➖ |  ➖ | ✅ |



# Tutorials
### [Main tutorial](https://huggingface.co/spaces/balthou/interactive-pipe-tutorial)
![tuto](/doc/images/tutorial.png)

[Tutorial on Hugging Face space](https://huggingface.co/spaces/balthou/interactive-pipe-tutorial)


[Tutorial in a Colab notebook](https://colab.research.google.com/github/livalgo/interactive-pipe-examples/blob/main/interactive_pipe_tutorial.ipynb#scrollTo=U38Oh7coVKwf)

### Learn by examples
#### [Basic image processing (python code sample for PyQT GUI)](/demo/multi_image.py)
| GUI | Pipeline | 
|:--:|:--:| 
|![](/doc/images/image_selector.png) | ![](/doc/images/image_selector_graph.png) |

![](/doc/images/demo_multi_image.gif)

#### [Speech exploration notebook (colab, signal processing)](https://colab.research.google.com/drive/1mUX2FW0qflWn-v3nIx90P_KvRxnXlBpz#scrollTo=qDTaIwvaJQ6R)
![Speech processing exploration in a notebook](/doc/images/jupyter_integration_speech.png)




## 🚀 Ultra short code


*Since ipywidgets in notebooks are supported, the tutorial is also available in a [google collab notebook](https://colab.research.google.com/drive/1PZn8P_5TABVCugT3IcLespvZG-gxnFbO?usp=sharing)*


Let's define 3 image processing very basic filters `exposure`, `black_and_white` & `blend`.

By design:
- image buffers inputs are arguments
- keyword arguments are the parameters which can be later turned into interactive widgets.
- output buffers are simply returned like you'd do in a regular function.

We use the `@interactive()` decorator to specify which parameters become interactive widgets. Parameters defined in the decorator as **tuple/list** will become graphical interactive widgets (slider, tick box, dropdown menu). 

The syntax to turn keyword arguments into sliders is pretty simple: `@interactive(param=(default, [min, max], name))` will create a float slider for instance.

Finally, we need to the glue to combo these filters. This is where the sample_pipeline function comes in.

By decorating it with `@interactive_pipeline(gui="qt")`, calling this function will magically turn into a GUI powered image processing pipeline.


```python
from interactive_pipe import interactive, interactive_pipeline
import numpy as np

@interactive(
    coeff=(1., [0.5, 2.], "exposure"),
    bias=(0., [-0.2, 0.2])
)
def exposure(img, coeff=1., bias=0.):
    '''Applies a multiplication by coeff & adds a constant bias to the image'''
    # In the GUI, the coeff will be labelled as "exposure". 
    # As the default tuple provided to bias does not end up with a string, 
    # the widget label will be "bias", simply named after the keyword arg. 
    return img*coeff + bias


@interactive(bnw=(True, "black and white"))
def black_and_white(img, bnw=True):
    '''Averages the 3 color channels (Black & White) if bnw=True
    '''
    # Special mention for booleans: using a tuple like (True,) allows creating the tick box.
    return np.repeat(np.expand_dims(np.average(img, axis=-1), -1), img.shape[-1], axis=-1) if bnw else img

@interactive(blend_coeff=(0.5, [0., 1.]))
def blend(img0, img1, blend_coeff=0.5):
    '''Blends between two image. 
    - when blend_coeff=0 -> image 0  [slider to the left ] 
    - when blend_coeff=1 -> image 1   [slider to the right] 
    '''
    return  (1-blend_coeff)*img0+ blend_coeff*img1

# you can change the backend to mpl instead of Qt here.
@interactive_pipeline(gui="qt", size="fullscreen")
def sample_pipeline(input_image):
    exposed = exposure(input_image)
    bnw_image = black_and_white(input_image)
    blended  = blend(exposed, bnw_image)
    return exposed, blended, bnw_image

if __name__ == '__main__':
    input_image = np.array([0., 0.5, 0.8])*np.ones((256, 512, 3))
    sample_pipeline(input_image)

```
❤️ This code shall display you a GUI with three images. The middle one is the result of the blend



Notes:
- If you write `@interactive()` with `def blend(img0, img1, blend_coeff=0.5):`, blend_coeff will simply not be a slider on the GUI.
- If you write `@interactive(blend_coeff=[0., 1.])` in the decorator, blend_coeff will be a slider initialized to 0.5
- If you write `@interactive(bnw=(True, "black and white", "k"))`, the checkbox will disappear and be replaced by a keypress event (press `k` to enable/disable black & white)

-----------
## 💡 Some more tips

```python
from interactive_pipe import interactive, interactive_pipeline, context
import numpy as np

COLOR_DICT = {"red": [1., 0., 0.],  "green": [0., 1.,0.], "blue": [0., 0., 1.], "gray": [0.5, 0.5, 0.5]}
@interactive(color_choice=["red", "green", "blue", "gray"])
def generate_flat_colored_image(color_choice="red"):
    '''Generate a constant colorful image
    '''
    flat_array =  np.array(COLOR_DICT.get(color_choice)) * np.ones((64, 64, 3))
    context["avg"] = np.average(flat_array)
    return flat_array
```

- Note that you can also create filters which take no inputs and simply "generate" images. 
- The `color_choice` list will be turned into a nice dropdown menu. Default value here will be red as this is the first element of the list!
----------

💡 Can filters communicate together?
Yes, using the `context` proxy from `interactive_pipe`. 
- Check carefully how we stored the image average of the flat image in context. 
- This value will be available to other filters.
`special_image_slice` is going to use that value to set the half bottom image to dark in case the average is high.

```python
def special_image_slice(img):
    out_img = img.copy()
    if context["avg"] > 0.4:
        out_img[out_img.shape[0]//2:, ...] = 0.
    return out_img
```
---


```python
@interactive(image_index=(0, [0, 2], None, ["pagedown", "pageup", True]))
def switch_image(img1, img2, img3, image_index=0):
    '''Switch between 3 images
    '''
    return [img1, img2, img3][image_index]
```
Note that you can create a filter to switch between several images. In `["pagedown", "pageup", True]`, True means that the image_index will wrap around. (it will return to 0 as soon as it goes above the maximum value of 2).

```python
@interactive(top_slice_black=(True, "special", "k"))
def black_top_image_slice(img, top_slice_black=True):
    out_img = img.copy()
    if top_slice_black:
        out_img[:out_img.shape[0]//2, ...] = 0.
    return out_img


@interactive_pipeline(gui="qt", size="fullscreen")
def sample_pipeline_generated_image():
    flat_img = generate_flat_colored_image()
    top_slice_modified = black_top_image_slice(flat_img)
    bottom_slice_modified_image = special_image_slice(flat_img)
    chosen = switch_image(flat_img, top_slice_modified, bottom_slice_modified_image)
    return chosen

if __name__ == '__main__':
    sample_pipeline_generated_image()
```

----------

### Release Notes


#### Version 0.8.9 (February 2026)

**New Features:**
- **Panel System**: Control panel layout and organization
  - Flexible panel positioning (left, right, top, bottom)
  - Detached control panels for separate windows
  - Nested panels and subpanels support
  - Grouped controls within panels
  - Improved spacing and borders for better visual organization
  - Full backend support (Qt, Gradio, matplotlib, notebook)

- **Table Data Type**: Display tabular data natively
  - Core Table functionality without external dependencies
  - Optional pandas DataFrame support for advanced use cases
  - Rendering support across all backends (Qt, Gradio, matplotlib)
  - Headerless tables option

- **TimeControl Enhancements**: Better time-based parameter control
  - Improved slider help display
  - Additional demos showcasing time-based animations

**API Improvements:**
- Context support at pipeline initialization
- Backend selection via enum (string format still supported)
- Graph visualization for GUI pipelines (press `G`)

**Deprecations:**
- Inline syntax deprecated (use decorator syntax instead)
- `output_canvas` argument removed
- Context aliases (`global_params`, `states` etc...) deprecated at initialization

#### Version 0.8.8 (January 2026)

**New Features:**
- **Clean Context API**: Access shared context directly without `global_params` pollution
  - `get_context()` - Get the shared context dictionary
  - `context` - Direct dict-like access to context
  - `layout` - Access layout configuration directly
  - `audio` - Access audio functionality directly

**Code Quality Improvements:**
- Replaced all assertions with proper exceptions (`ValueError`, `TypeError`, `RuntimeError`)
- Fixed all mutable default arguments across the codebase (prevents shared state bugs)
- Improved type hints with proper `Optional` and `Any` types
- Better error messages for debugging

**UX Improvements:**
- Dropdown menus are now hidden when only a single choice is available
- Helpful message displayed when Graphviz is not available (when pressing `G`)
- Fixed warning in linestyle for curves

**Bug Fixes:**
- Fixed audio initialization order in Qt backend
- Fixed pytest failures for optional dependencies in CI
- Fixed various edge cases in error handling

**Migration from old `context={}` or `global_params={}` patterns:**  

```python
# OLD (deprecated) - using context={} or global_params={}
from interactive_pipe import interactive

@interactive(brightness=(0.5, [0., 1.]))
def apply_brightness(img:np.ndarray, brightness: float = 0.5, global_params={}):
    global_params["brightness"] = brightness  # Storing shared data
    global_params["__output_styles"]["output"] = {"title": "Brightened"}  # Setting layout
    return img * brightness

# NEW (recommended) - using context and layout proxies
from interactive_pipe import interactive, context, layout

@interactive(brightness=(0.5, [0., 1.]))
def apply_brightness(img:np.ndarray, brightness: float = 0.5):
    context["brightness"] = brightness  # or: context.brightness = brightness
    layout.style("output", title="Brightened")
    return img * brightness
```

The new API provides:
- `context` - For sharing data between filters (replaces `global_params["key"]`)
- `layout` - For controlling output display (replaces `global_params["__output_styles"]`)
- `audio` - For audio playback control
- `get_context()` - Get the shared context dictionary directly




**License:**
- Updated to MIT License

---

### History
- Interactive pipe was initially developed by [Balthazar Neveu](https://github.com/balthazarneveu) as part of the [irdrone project](https://github.com/wisescootering/infrareddrone/tree/master/interactive) based on matplotlib.
- Later, more contributions were also made by [Giuseppe Moschetti](https://github.com/g-moschetti) and Sylvain Leroy.
- August 2023: rewriting the whole core and supporting several graphical backends!
- September 2024: Gradio backend
- January 2026: Clean Context API and code quality improvements (v0.8.8)


### FAQ
- ❓ What is the recommended way to access shared context?
> **New in v0.8.8**: Use the clean context API for direct access:
> ```python
> from interactive_pipe import context, layout, audio, get_context
> 
> @interactive()
> def my_filter(img):
>     context["shared_key"] = "shared_value"  # Direct dict-like access
>     context.brightness = 0.5
>     layout.set_title("output_image", "My Image")  # Layout helpers
>
>     return img
> ```

- ❓ How do I change the layout?  *Can I change the grid layout of images live? (like you compare 2 images side by side and you want to start comparing 4 images in a 2x2 fashion for debugging purpose)*. It is possible with Qt backend. 

> Use the `layout` helper to control image arrangement and styling:
> ```python
> from interactive_pipe import layout
> 
> def change_layout(layout: str="side_by_side"):
>     # Arrange outputs in a 2x2 grid
>       if layout == "side_by_side":
>            layout.grid([["input", "result"]])
>       if layout == "grid2x2":
>           layout.grid([["input", "processed"], ["histogram_graph", "result"]])
>     # Style individual outputs
>     layout.style("result", title="Final Result")
>     # Note that the string "input", "processed", "histogram_graph", "result"
>     # are the variables used in the pipeline (see below)!
> 
> def pipeline(input):
>     processed = denoise(input)
>     result = change_brightness(processed)
>     histogram_graph = compute_histo(processed)
>     change_layout()
>     return result
> ```

- ❓ Do I have to remove `KeyboardSlider` when using gradio or notebook backends?
> No, don't worry, these will be mapped back to regular sliders!
- ❓ How do I play audio live?
> 🔊 Inside a processing block, write the audio file to disk and use the audio helper:
> ```python
> from interactive_pipe import audio
> audio.set_audio(audio_file)  # New clean API (v0.8.8)
> # or legacy: context["__set_audio"](audio_file)
> ```
- ❓ Do I have to decorate my processing block using the `@interactive` 
> If you use the `@` decoration style, your function won't be useable in a regular manner (wich may be problematic in a serious development environment)
```python
@interactive(angle=(0., [-360., 360.]))
def processing_block(angle=0.):
    ...
```

> An alternative is to decorate the processing block outside... in a file dedicated to interactivity for instance
```python
# core_filter.py
def processing_block(angle=0.):
    ...
```

```python
# graphical.py
from core_filter import processing_block

def add_interactivity():
    interactive(angle=(0., [-360., 360.]))(processing_block)
```
- ❓ Can I call the pipeline in a command line/batch fashion?
> Yes, headless mode is supported. 🔜 documentation needed.


- ❓ Can I use inplace operations?
> Better avoid these in general. To avoid making extra copies, computing hashes everywhere and avoid loosing precious computation time, there are no checks that inputs are not modified in place.
```python
# Don't do that!
def bad_processing_block(inp):
    inp+=1
```

- ❓ Is there a difference between `global_params` and `context` ?
> No, `global_params`, `global_parameters`, `global_state`, `global_context`, `context`, `state` all mean the same thing and are all supported for legacy reasons. `context` is the preferred wording. However, we now recommend using the clean context API (see above).
> ⚠️ The old `global_params={}` / `context={}` keyword argument style still works for backwards compatibility but is deprecated.



# Roadmap and todos
🐛 Want to contribute or interested in adding new features? Enter a new [Github issue](https://github.com/balthazarneveu/interactive_pipe/issues)

🎁 Want to dig into the code? Take a look at [code_architecture.md](/code_architecture.md)



----------------------

### Development

#### Code quality checks

Before committing, ensure your code passes the linters, type checker, and tests. The CI runs these checks automatically:

**What CI does:**
- **Ruff formatting check** (`.github/workflows/ruff-format.yaml`): Runs `ruff format --check` to verify code formatting
- **Ruff linting** (`.github/workflows/ruff-lint.yaml`): Runs `ruff check` to check code quality (replaces flake8)
- **Pyright type checking** (`.github/workflows/pyright.yaml`): Runs `pyright` for static type checking (informational, non-blocking)
- **Pytest tests** (`.github/workflows/pytest.yaml`): Runs `pytest` on Python 3.9, 3.10, and 3.11

**Local commands (match CI):**

```bash
# Install development tools and test dependencies
pip install -e ".[dev,pytest]"

# Format code (Ruff) - matches CI
ruff format .

# Check formatting (Ruff) - matches CI
ruff format --check .

# Lint code (Ruff) - matches CI (auto-fixes when possible)
ruff check .

# Auto-fix linting issues (Ruff)
ruff check --fix .

# Type check (Pyright) - matches CI (informational)
pyright src/

# Run tests (pytest) - matches CI
pytest

# Pre-commit checklist (run all before committing)
ruff format .
ruff check --fix .
pyright src/  # Optional, won't block commit
pytest
```

**Note:** Ruff replaces both Black (formatting) and Flake8 (linting) in a single, faster tool. Pyright provides static type checking to catch type errors early.