Metadata-Version: 2.4
Name: pyviewer
Version: 2.2.1
Summary: Interactive Python viewers
Author-email: Erik Härkönen <erik.harkonen@hotmail.com>
License-Expression: CC-BY-NC-SA-4.0
Project-URL: Homepage, https://github.com/harskish/pyviewer
Project-URL: Source, https://github.com/harskish/pyviewer
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: glfw<3,>=2.8
Requires-Dist: numpy
Requires-Dist: pyopengl<4,>=3.1.7
Requires-Dist: python-wayland<2,>=1
Requires-Dist: imgui-bundle<1.93,>=1.92.5
Requires-Dist: setuptools>=77
Requires-Dist: light-process<0.1,>=0.0.7
Requires-Dist: py<2,>=1.11
Provides-Extra: custom-ops
Requires-Dist: ninja; extra == "custom-ops"
Dynamic: license-file

# PyViewer

![Toolbar Viewer](docs/screenshot.jpg)

Pyviewer is a python library for easily visualizing NumPy arrays and PyTorch tensors.

## Components

### single_image_viewer.py

A viewer for showing single fullscreen images or line plots without other UI elements. Runs in a ***separate process*** and remains interactive even if the main process is suspended (e.g. in a debugger). Great for interactively looking at intermediate values of complex ML/CG/CV pipelines.

Usage:
```
from pyviewer import single_image_viewer as siv
siv.draw(img_chw=np.random.randn(3,64,64))
siv.plot(np.sin(2*np.pi*np.linspace(0, 1, 10_000)))
```

### toolbar_viewer.py
A viewer that shows ImGui UI elemets on the left, and a large image on the right. Runs in the main process, but supports visualizing torch tensors directly from GPU memory (unlike single_image_viewer).

### tensor_viewer.py

A viewer for large, multidimensional PyTorch tensors. Runs in a separate thread
in the same process with `pydev_do_not_trace = True`, keeping its UI interactive
at PyDev/debugpy breakpoints. Dimension radio buttons choose X, Y and an optional
channel axis; sliders select the remaining slice indices and R/G/B channels.
The image, axes controls, and GPU timings are separate dockable windows; use
the View menu to reopen a closed window. Unlock the top-right L/U button to
reveal the UI scale slider (0.1 to 5.0);
right-click the slider to reset to 1.0.
Drag to pan, scroll to zoom, double-click to reset, or right-click to snap scale.
Press P/Pause to toggle updates and N/Space to accept one tensor while paused.
While paused, rendering drops to 5 FPS after two seconds without input and stops
entirely after three minutes. Mouse movement, clicks, scrolling, or keyboard input
wakes the viewer. Tensor publication does not reset the inactivity timer; new
references remain pending while rendering is stopped. Unpaused rendering continues
normally.

```python
from pyviewer import tensor_viewer as tv

# NCHW: show batch index 0 and choose the displayed RGB channels.
tv.draw(tensor, x_dim=3, y_dim=2, channel_dim=1, channels=(0, 1, 2))
# Or select a grayscale plane, defaulting to the final two dimensions as Y/X.
tv.draw(other_tensor)
tv.inst.wait_for_close()  # optional: keep the script alive until the window closes
```

After Esc or window close, `tv.draw()` and `tv.init()` leave the viewer closed.
Call `tv.show_window()` (or `tv.inst.restart()`) to open it again.

The viewer retains a detached reference to the tensor, including its original
device and strides. Cropping, integer-stride slicing and transposing create views;
only the visible samples are packed for texture upload. CUDA tensors use direct
CUDA–GL interop; CPU tensors upload from host memory. Each
texture dimension is bounded by the framebuffer size and `GL_MAX_TEXTURE_SIZE`.
Zoomed-out sampling approximates nearest sampling on a regular integer lattice;
zoomed-in sampling uses every visible source pixel. Automatic normalization uses
visible samples only, so the intensity range can change while panning. Turn it
off in the controls (or use `tv.init(normalize=False)`) to select a fixed display
range with the two-value slider. Its defaults are [0, 1] for float tensors and
[0, 255] for uint8 tensors. The float slider spans [-2, 2]; Ctrl-click a slider
value to enter a wider limit. Turning automatic normalization off copies its last
visible sample limits into the sliders.

Selections persist across calls with the same shape when selection arguments are
omitted. In-place updates appear on subsequent frames; coordinate concurrent
writes if a consistent frame is needed. For CUDA, finish producing the tensor
before publishing it (for example with `torch.cuda.synchronize()`), as the viewer
uses its own thread and stream. The UI thread is a daemon, so the script must
remain running to keep the window open. Use one active threaded viewer at a time;
GLFW/ImGui have process-global state. Background GLFW window handling works on
Linux/Windows; macOS requires it on the main thread.

## Other features
* Makes use of [imgui-bundle](https://github.com/pthom/imgui_bundle) to provide plotting support and other extras
* Dynamically rescalable user interface
* Window resizing to integer multiple of content resolution
* Pan and zoom of the main image

## Installation
Install with `pip install pyviewer`. To use the optional Torch custom interop
operations, install `pip install 'pyviewer[custom-ops]'` so that the Ninja build
tool is available. <br>
Requires **Python 3.11+**.

### Nix

The development shell provides pyviewer and its Python dependencies as
Nix-managed packages:

```sh
nix develop
```

Python 3.13 is the default. Use `nix develop .#python311`, `.#python312`, or
`.#python314` to select another supported interpreter.
Entering a development shell refreshes `.nix-python/bin/python`, a local link
to the shell's Python for VS Code interpreter selection and debugging.

Build the package with `nix build`. The legacy `nix-shell` and
`nix-build default.nix -A pyviewer` entry points are also available.
Versioned flake packages such as `nix build .#pyviewer313` are provided for
Python 3.11 through 3.14. With the legacy shell, select a version with
`nix-shell --argstr pythonVersion 3.12`.

The package set can also be imported by another Nix expression:

```nix
pyviewerPackages = import /path/to/pyviewer {
  inherit pkgs;
  pythonPackages = pkgs.python312Packages;
};
```

Use `pyviewerPackages.pyviewer` in `python.withPackages`. The Nix package set
uses pinned upstream `imgui-bundle` wheels, currently available for CPython
3.11 through 3.14 on x86_64 Linux and x86_64/aarch64 macOS 14 or newer.

## Usage
See `examples/demo.py` for a usage example.

## API highlights
`PannableArea::screen_to_uv_xform`<br>
Maps absolute screen coordinates (e.g. `imgui.get_mouse_pos()`) to transformed image UVs, useful for picking etc.<br>

`PannableArea::uv_to_screen_xform`<br>
Maps image UVs to absolute screen coordinates. Useful when combined with imgui's [draw lists](https://pyimgui.readthedocs.io/en/latest/reference/imgui.core.html#imgui.core._DrawList).

`PannableArea::get_visible_box_image()`<br>
Returns the top-left and bottom-right UV coordinates of the currently visible image region.<br>

`PannableArea::get_hovered_uv_image()`<br>
Returns the image UVs that lie under the mouse cursor.

`PannableArea::get_hovered_uv_canvas()`<br>
Returns the *canvas* UVs that lie under the mouse cursor. Differs from the image UVs in case of non-matching image and window aspect ratios.

`from pyviewer.single_image_viewer import draw; draw(img_chw=...)`<br>
One-liner that opens a new viewer (unless already open) and draws the provided image. Runs in a separate process and thus works even when execution is halted by e.g. a debugger.
