Metadata-Version: 2.4
Name: twhy
Version: 0.3.0
Summary: Zoomable microscopy, image and PDF previews in a text terminal
Author: Jason L Weirather
License-Expression: Apache-2.0
Project-URL: Repository, https://github.com/jason-weirather/twhy
Project-URL: Issues, https://github.com/jason-weirather/twhy/issues
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click<9,>=8.1
Requires-Dist: coolname<6,>=2.2
Requires-Dist: numpy<3,>=1.26.4
Requires-Dist: scipy<2,>=1.14
Requires-Dist: omeify>=0.23.1
Requires-Dist: pillow<13,>=11.3
Requires-Dist: pypdfium2<6,>=4.30
Requires-Dist: prompt-toolkit<4,>=3.0.52
Requires-Dist: wcwidth>=0.2.13
Provides-Extra: s3
Requires-Dist: omeify[s3]>=0.23.1; extra == "s3"
Provides-Extra: dev
Requires-Dist: pytest<10,>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: pytest-cov>=5; extra == "dev"
Requires-Dist: pexpect>=4.9; sys_platform != "win32" and extra == "dev"
Requires-Dist: ruff>=0.12; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Dynamic: license-file

# twhy

**Whole-slide microscopy, ordinary images, and scientific PDFs, right in your terminal.**

`twhy` is a zoomable, full-screen image viewer made of *actual terminal characters*. Open an OME-TIFF, SVS, or OME-Zarr slide, explore multichannel fluorescence and Z/T stacks, inspect a PNG, or page through a PDF. No browser, image server, X11, graphics protocol, or GPU required.

I had a use case for this on a remote cluster with thousands of images. Sometimes
I just needed the tissue's shape, a rough count of regions of interest, or to tell
a big chunk of tissue from a few tiny core needle biopsies. An ASCII-art
representation of a preview from the image pyramid seemed like it would do the job.

Then working in tmux, switching the active area of the terminal with either the
mouse or keyboard, got me wondering... just how far can we push a text-based image
viewer? And thus began `twhy`. I've been having a little fun with AI slop along
the way, and now it opens PDFs and lets me draw polygons on pathology images.
It's ridiculous, but I'm actually going to use it.

<p align="center">
  <a href="https://github.com/jason-weirather/twhy/blob/main/docs/media/pathology.gif"><img src="https://raw.githubusercontent.com/jason-weirather/twhy/main/docs/media/pathology.gif" width="900" alt="Real recording of twhy zooming through multiplex immunofluorescence and revealing CD8 signal in cyan on blue DAPI nuclei"></a>
</p>
<p align="center"><em>Real terminal footage: 59-channel multiplex microscopy, DAPI and CD8. Channel selection and zoom, no mock interface.</em></p>

## See it in action

These are short, automatically looping recordings of `twhy` itself. Click any clip to view it at full GIF size.

<table>
<tr>
<td width="50%" valign="top">
<strong>Whole-slide H&amp;E SVS</strong><br>
<a href="https://github.com/jason-weirather/twhy/blob/main/docs/media/he.gif"><img src="https://raw.githubusercontent.com/jason-weirather/twhy/main/docs/media/he.gif" width="440" alt="twhy zooming from an SVS whole-slide tissue overview to individual nuclei in a real H&E image"></a><br>
<small>Zoom from whole-slide tissue context into individual nuclei.</small>
</td>
<td width="50%" valign="top">
<strong>An ordinary PNG. Featuring a cat.</strong><br>
<a href="https://github.com/jason-weirather/twhy/blob/main/docs/media/cat.gif"><img src="https://raw.githubusercontent.com/jason-weirather/twhy/main/docs/media/cat.gif" width="440" alt="twhy zooming into an orange-and-white cat in a sunlit windowsill illustration"></a><br>
<small>PNG, JPEG, WebP: zoom and pan familiar images, too.</small>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>A scientific PDF</strong><br>
<a href="https://github.com/jason-weirather/twhy/blob/main/docs/media/pdf.gif"><img src="https://raw.githubusercontent.com/jason-weirather/twhy/main/docs/media/pdf.gif" width="440" alt="twhy switching pages in a scientific PDF and zooming into a transformer architecture diagram"></a><br>
<small>Turn pages, then zoom into figures. PDF support is standard.</small>
</td>
<td width="50%" valign="top">
<strong>Polygon selections</strong><br>
<a href="https://github.com/jason-weirather/twhy/blob/main/docs/media/selection.gif"><img src="https://raw.githubusercontent.com/jason-weirather/twhy/main/docs/media/selection.gif" width="440" alt="twhy drawing a yellow polygon selection around a tissue core and preparing it for GeoJSON export"></a><br>
<small>Select rectangles or polygons; export GeoJSON, text, ANSI art, or PNG.</small>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<strong>Animated GIF in terminal ASCII</strong><br>
<a href="https://github.com/jason-weirather/twhy/blob/main/docs/media/horse-animation.gif"><img src="https://raw.githubusercontent.com/jason-weirather/twhy/main/docs/media/horse-animation.gif" width="440" alt="Real twhy recording playing Muybridge's galloping horse as moving ASCII character art"></a><br>
<small>Eadweard Muybridge's galloping horse, replayed as real terminal characters.</small>
</td>
<td width="50%" valign="top">
<strong>Live-cell microscopy: Z and T</strong><br>
<a href="https://github.com/jason-weirather/twhy/blob/main/docs/media/mitosis-zt.gif"><img src="https://raw.githubusercontent.com/jason-weirather/twhy/main/docs/media/mitosis-zt.gif" width="440" alt="Real twhy terminal recording of mitotic chromosomes in an IDR OME-Zarr Z/T stack"></a><br>
<small>Fluorescence time series, navigating an IDR mitosis Z-stack.</small>
</td>
</tr>
</table>

## Install

Python **3.11+**. The **PyPI distribution**, terminal command, and Python import all share the name `twhy`.

Install the standalone command without changing packages in a scientific Python environment:

```bash
uv tool install twhy
# or: pipx install twhy
# or: python -m pip install twhy
```

From a cloned checkout, for development:

```bash
python -m pip install -e '.[dev]'
```

Z/T navigation uses **Omeify 0.23.1 or newer**, installed automatically as a
package dependency. [Z/T navigation and examples](docs/z_t_support.md).

PDF support is **included** in the standard install. No special extra or system PDF converter is needed.

## Quickstart

```bash
twhy photo.png                         # PNG, JPEG, WebP, and more
twhy paper.pdf                         # page navigation and progressive detail
twhy slide.svs                         # Aperio whole-slide RGB
twhy slide.ome.tif                     # OME-TIFF, grayscale or multichannel
twhy /data/sample.ome.zarr             # local OME-Zarr
twhy time-depth.ome.tif                # middle Z, T=0; d chooses navigation axis
twhy experiment.zarr --z-index 15 --t-index 12  # explicit zero-based startup plane
twhy animation.gif                     # animated GIF plays and loops automatically
twhy time-depth.ome.tif --play-fps 3   # T slideshow rate; press P to play

# Compose two fluorescence channels with chosen colors:
twhy slide.ome.tif --channel 0 --channel 2 \
  --channel-color 0 blue --channel-color 2 cyan
```

The initial view fits the image. Wheel zoom is anchored to the pointer; pan with the mouse or arrow keys. The terminal sidebar keeps channels, colors, and contrast controls at hand.

**Navigating multidimensional images:** Press **`d`** to choose which available dimension you
are stepping through: **Z**, **T**, or **Series** (for a file with multiple image
series). Use **`[` / `]`** or PageUp/PageDown to step; **`p`**, a zero-based
index, and Enter to jump. You can also click Z/T/Series in the top navigation
bar. For example, in a two-series OME-TIFF with Z/T in each series, select
Series with `d`, step into series 1, then select Z or T to browse that series.
Each series has its own dimensional extents. Switching series fits the new image,
resets its channel selection, and starts at middle Z and T=0; stepping Z/T inside
one series preserves your camera and display limits. **`H`** opens the focused
channel's histogram, with draggable lower/upper display limits; **`l`/`u`/`g`**
remain the primary direct contrast/gamma controls in the image view.

Z/T changes preserve the camera, channels, colors and contrast. OME time series
start paused; press **P** for best-effort looping slideshow playback, **R** to set
frames/second. Animated GIFs play and loop by default using their encoded delays.
Manual stepping pauses playback. A new series still fits and initializes its own channels. Omitted startup indices use middle Z
(`size // 2`) and T=0 for present axes; invalid explicit indices fail. Only exact-section
pyramid levels are used automatically, including when the input stores an XYZ
pyramid. [Navigation, playback, limitations, and public test inputs](docs/z_t_support.md).


## Why a terminal viewer?

- **Whole-slide without whole-slide RAM use:** regional TIFF/SVS reads and pyramids, with bounded previews and useful zoomed-out overviews.
- **Multichannel, not just RGB:** select channels, change pseudocolors, and adjust each marker's display window and gamma without modifying source pixels.
- **PDFs and everyday images:** ordinary images use in-memory pyramids; PDFs refine progressively from a quick preview to higher-detail pages.
- **Real characters:** colored `▀` half-blocks, Braille detail, shape-oriented Unicode art, and portable ASCII modes.
- **Selection and export:** copy/save terminal text, colored ANSI, shareable PNGs, and GeoJSON polygons/rectangles.
- **Reusable Python library:** sampling, intensity normalization, composition, and rendering are available without opening the interactive terminal UI.

`twhy` works over ordinary terminal sessions, including environments where a desktop image viewer is inconvenient. It is an exploratory display, **not a color-managed diagnostic viewer** and not a substitute for measuring the original pixels.

## The keys you'll actually use

| Action | Key or gesture |
|---|---|
| Zoom; pan; fit image | Wheel or `+`/`-`; arrows or drag; `r`/Home |
| Choose navigation dimension | `d`, or click Z / T / Series in the navigation bar |
| Step/jump within that dimension | `]` / `[` or PageDown / PageUp; `p` to jump (PDF pages remain one-based) |
| GIF / time playback | `P` plays/pauses; `R` sets 0.1–30 frames/s; animated GIFs loop by default |
| Toggle a channel; focus another | Click channel/Space; `,` and `.` |
| Inspect histogram / refine contrast | `H` toggles histogram; drag its lower/upper range handles; `L` toggles log counts |
| Adjust channel color or contrast | `h` (hue); `l`/`u` (limits); `g` (gamma); `a` (preview auto window) |
| Switch artwork / strict ASCII | `m` / `M`; `t` toggles artwork color; `f` toggles fullscreen |
| Select region | `s` rectangle; `v` polygon |
| Export a selection | `y/Y` text, `i/I` PNG, `t/T` ANSI, `j/J` GeoJSON (copy/save) |
| Help or quit | `?` / `q` |

## Channel histograms and display contrast

Press **H** to inspect the focused channel's intensity distribution. Drag the
**^L / ^U** markers to adjust the display range, or enter exact limits with
**l / u**; **g** adjusts gamma. **L** switches between logarithmic and linear
histogram counts. These controls affect only the display, not the source pixels.

twhy starts preparing the focused channel after a short pause, so opening
**H** can reuse a ready histogram. Scrolling through channels does not queue
work for every channel you pass. Time-series playback takes priority over this background work.
Display limits stay fixed as you move through Z, T, or the image; preparation
never changes them behind your back. Histograms of large images are estimates,
not exhaustive pixel counts.

In the histogram view, **A** offers an experimental suggested display range,
applied only when you ask. Lowercase **a** retains its existing visible-preview
auto-window behavior. Use `--no-stack-contrast` to turn off automatic preparation.

[Method, Python API, sampling limits and caveats](docs/contrast-estimation.md).

## From terminal annotations to full-resolution ROIs

The useful part of the joke: draw regions in `twhy`, save their coordinates as
GeoJSON, and let [Omeify](https://github.com/jason-weirather/omeify) extract them
from the original image. This example uses a calibrated OME-TIFF, series 0.
You also need the `omeify` command on your PATH; with isolated tool installs,
install it separately using `uv tool install omeify`.

```bash
# Open the source and choose where to save the annotations.
twhy slide.ome.tif --series 0 --output-geojson-clip regions.geojson
```

Zoom to the view you need, then press **`s`** for a rectangle or **`v`** for a
polygon and draw. Close a polygon with **Enter**; **`a`** adds another region in
the same view. Press **Shift+J** to save all the regions before quitting.

```bash
# Export one full-resolution image per region, with lossless compression.
omeify crop slide.ome.tif --series 0 \
  --geojson regions.geojson -o regions.ome.tif \
  --shatter by_index --compression Deflate --no-overwrite
```

This writes `regions-01.ome.tif`, `regions-02.ome.tif`, and so on, retaining all
channels from the selected series. Omit `--shatter by_index` to put the regions
into separate image series in one OME-TIFF instead.

**Full-resolution pixels, not a screenshot:** Omeify reads the original image,
not the terminal preview. Omeify writes each polygon's enclosing rectangle and
masks pixels outside the polygon by default; keep the GeoJSON for the outlines.
Use the same source image,
series, Z/T plane, and orientation in both steps. Z/T indices are not embedded
in the anonymous GeoJSON; record them and pass matching `--z-index`/`--t-index`
to Omeify when cropping a stack. Annotation placement follows the terminal
grid, so zoom in before drawing for finer boundaries.
[More crop options](https://github.com/jason-weirather/omeify/blob/main/docs/regions.md).

## Supported files

| Format | What twhy does |
|---|---|
| OME-TIFF | Regional microscopy viewing, channels, series, Z slices and time points |
| Aperio SVS | RGB whole-slide viewing using available stored levels |
| OME-Zarr | Local/HTTP(S)/optional S3, regional reads, series and Z/T navigation |
| PNG, JPEG, WebP, BMP, TGA, PPM | Ordinary raster viewing with zoom and pan |
| Animated GIF | Loop by default using GIF delays, with frame stepping and a bounded prepared-frame cache |
| APNG, animated WebP | View a selected frame; interactive playback is not implemented |
| PDF | Navigate pages; progressively improve raster detail; copy embedded text from a rectangular selection |

For advanced flags, channel/series semantics, color modes, cache limits, clipboard behavior, and format caveats, use the **[full reference](https://github.com/jason-weirather/twhy/blob/main/docs/reference.md)**. Architecture notes are in [docs/architecture.md](https://github.com/jason-weirather/twhy/blob/main/docs/architecture.md).

## Try a synthetic microscopy example

The repo includes a deterministic fixture generator that creates public-safe, synthetic tissue-like RGB and multichannel OME-TIFFs. It is a renderer demonstration, not a biological simulation.

```bash
python examples/make_demo.py --output-directory /tmp/twhy-demo
twhy /tmp/twhy-demo/synthetic-mif.ome.tif --channel 0 --channel 2
```

### Try a multi-series Z/T OME-TIFF

To try a TIFF containing two separate image series, each with its own Z and T
axes, use the included synthetic example:

```bash
python examples/make_multiseries_zt_demo.py \
  -o ~/Scratch/twhy-multiseries-zt.ome.tif
twhy ~/Scratch/twhy-multiseries-zt.ome.tif
```

Press `d` until **Series** is selected, then `]` to change series. Choose Z or T
with `d` to navigate the stack within it. No input microscope file is needed.

## Python API

```python
import numpy as np
from twhy import TextRenderer

pixels = np.linspace(0, 1, 256**2).reshape(256, 256)
frame = TextRenderer(mode="detail", color=False).render(pixels, columns=80)
print(frame.to_text())
```

## Optional remote sources

Local files are the main workflow. `twhy` also supports HTTP(S) images/PDFs (bounded download) and HTTP(S) OME-Zarr (regional chunk reads). For native S3 or S3-compatible stores, install `twhy[s3]` and use your normal AWS credentials or an endpoint override. No S3 setup is needed for local images, slides, or PDFs. [Remote input reference](https://github.com/jason-weirather/twhy/blob/main/docs/reference.md#local-and-remote-inputs).

## Release media and provenance

These GIFs are edited recordings of the real `twhy` terminal viewer. Showcase sources:

- **mIF:** Tan, Y. & Kempchen, T. (2024), [*SPACEc demo dataset*](https://doi.org/10.5061/dryad.brv15dvj1), Dryad.
- **H&E whole-slide demo:** `CMU-1.svs`, [OpenSlide test data](https://openslide.cs.cmu.edu/download/openslide-testdata/Aperio/), Carnegie Mellon University, CC0 1.0.
- **PDF:** Vaswani, A. et al. (2017), [*Attention Is All You Need*](https://arxiv.org/abs/1706.03762), NeurIPS.
- **Cat:** Illustration generated with ChatGPT (OpenAI).
- **Galloping horse:** Eadweard Muybridge, *Animal Locomotion* (1887), plate 626;
  animation assembled by Wikimedia Commons contributor Waugsberg (2006),
  [source and public-domain status](https://commons.wikimedia.org/wiki/File:Muybridge_race_horse_animated.gif).
  The showcase is a recording of twhy playing that public-domain GIF.
- **Mitotic chromosomes (Z/T):** Walther N et al. (2018), *A quantitative map of
  human Condensins provides new insights into mitotic chromosome architecture*,
  *J. Cell Biol.* [doi:10.1083/jcb.201801048](https://doi.org/10.1083/jcb.201801048).
  Image Data Resource [idr0052, image 5514375](https://idr.openmicroscopy.org/study/idr0052/),
  [data DOI:10.17867/10000123](https://doi.org/10.17867/10000123),
  [OME-NGFF sample catalog](https://idr.github.io/ome-ngff-samples/), **CC BY 4.0**.
  The showcase is a recording of twhy viewing a derived OME-Zarr representation.

## License

Apache-2.0. See [LICENSE](https://github.com/jason-weirather/twhy/blob/main/LICENSE).
