Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ This project follows a pragmatic versioning approach:
## 2.0.0 - 2026-06-19

- Added
- **Image pixel interpolation** (`--i-pixels` flag, `pixels.py`, `frame.py`): new global flag `--i-pixels` adds bilinear interpolation between pixels to upscale final images; produces smoother, higher-resolution output from the same fractal computation; valid range 0–3 (0 = no interpolation, 3 = 4× resolution); effectively multiplies final image dimensions by `(i_pixels + 1)` so a 1024×1024 render with `--i-pixels 2` produces a 3072×3072 PNG; implemented via deterministic numpy-based bilinear interpolation using `PIL.Image.Resampling.BILINEAR`; metadata key `tranZoom:render:i_pixels` stores the interpolation level in PNG text chunks.
- **Image pixel interpolation** (`--i-pixels` flag, `pixels.py`, `frame.py`): new global flag `--i-pixels` adds interpolation between pixels to upscale final images; produces smoother, higher-resolution output from the same fractal computation; valid range 0–3 (0 = no interpolation, 3 = 4× resolution); effectively multiplies final image dimensions by `(i_pixels + 1)` so a 1024×1024 render with `--i-pixels 2` produces a 3072×3072 PNG; implemented via deterministic numpy-based interpolation using PIL's `Resampling` algorithms; default method is bilinear for best stability; metadata key `tranZoom:render:i_pixels` stores the interpolation level in PNG text chunks.
- **Pixel interpolation resampling method** (`--resample` flag, `pixels.py`): new global flag `--resample METHOD` allows selection of interpolation algorithm for pixel upscaling; available methods include bilinear (stable and fast, recommended), bicubic (smoother but slightly slower), lanczos (maximum quality, slowest), and others from PIL's `Resampling` enum; default is lanczos; choice persists in metadata, allowing re-renders with same settings to maintain consistency; particularly useful for tuning final output quality when combined with `--i-pixels`.
- **Animation frame interpolation** (`--i-frames` flag, `pixels.py`, `zoom.py`): new `tranz zoom auto` flag `--i-frames` generates interpolated frames between each real fractal-computed frame; produces smoother, higher-FPS animations from fewer fractal computations; valid range 0–7 (0 = no interpolation, 7 = 8× FPS); effectively multiplies final FPS by `(i_frames + 1)` so `--fps 10 --i-frames 2` produces 30 effective FPS; supports both linear interpolation (default for first/last frame pairs) and **quadratic interpolation** (uses curr, next, next+1 frames for smoother acceleration); `InterpolatedFrameStream()` yields real + interpolated frames in animation order; metadata key `tranZoom:zoom:frame:i_frames` stores the interpolation level.
Comment thread
balparda marked this conversation as resolved.
- **New pixels.py module** (`pixels.py`): extracted all pixel rendering logic from `image.py` into dedicated 1440-line module; handles color palette application, histogram equalization, PNG/GIF/MP4 encoding, mark/overlay drawing, and pixel interpolation; new classes `RenderParameters` (single-image rendering), `RenderAnimationParameters` (animation rendering), `Pixels` (raw RGBA pixel array), `RenderedZoomFrame` (rendered animation frame with metadata); separates fractal computation concerns (escape-time iteration, arbitrary precision) from rendering concerns (color mapping, file I/O, interpolation).
- **Quadratic frame interpolation** (`zoom.py`): new `QuadraticInterpolatedFrame()` function uses three-point quadratic interpolation for smoother frame blending than linear interpolation; takes `(curr, next, next+1)` frames to compute intermediate frame with acceleration/deceleration curve; blends RGB pixel values via quadratic Lagrange polynomial; automatically falls back to linear interpolation for last frame pair or when `use_quadratic=False`; controlled by `DEFAULT_USE_QUADRATIC` constant (default True).
Expand Down
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ The tool can save all computations to a local DB. If allowed, it will use these
- **Manual zoom session**: The `tranz zoom manual` command runs the same iterative frame navigation but prompts the user for a direction at each step (1–9, numpad layout: 5=center, 8=N, 6=E, etc.) instead of querying an LLM. Supports both Mandelbrot and Julia Set fractals.
- **Sector scoring**: Each sector is scored on a 0–100 scale for `fractal_score` (visual complexity / zoom promise). When targeted search is active, an additional `target_match_score` (also 0–100) is blended in with a configurable weight.
- **Image metadata**: All tranZoom PNG images embed rich metadata (`tranZoom:*` PNG text chunks) including frame coordinates, magnification, palette (`tranZoom:render:palette`), precision, per-pixel statistics (`n:min`, `n:max`, `nu:min`, `nu:max`, histogram summaries), and (for AI/manual sessions) the full LLM evaluation, model parameters, prompts, and zoom step count.
- **Pixel interpolation** (`--i-pixels`): Bicubic upscaling applied to the final rendered image to produce higher-resolution output without additional fractal computation. The `--i-pixels N` flag (valid range 0–3) inserts `N` interpolated pixels between each computed pixel in both dimensions, multiplying the final image size by `(N+1)`. For example, `--i-pixels 2` transforms a 1024×1024 computation into a 3072×3072 final image using deterministic bicubic interpolation. The fractal is computed once at the base resolution, then upscaled using weighted RGBA blending over 2×2 pixel neighborhoods. This produces smoother gradients and reduces visible pixelation at the cost of larger file sizes; no additional computation time beyond the interpolation pass itself (≪1 second for typical sizes). Metadata key `tranZoom:render:i_pixels` stores the interpolation level. Use this when you want poster-quality output from moderate computation cost.
- **Pixel interpolation** (`--i-pixels`): Lanczos/Bicubic/Bilinear upscaling applied to the final rendered image to produce higher-resolution output without additional fractal computation. The `--i-pixels N` flag (valid range 0–3) inserts `N` interpolated pixels between each computed pixel in both dimensions, multiplying the final image size by `(N+1)`. For example, `--i-pixels 2` transforms a 1024×1024 computation into a 3072×3072 final image using Lanczos interpolation. The fractal is computed once at the base resolution, then upscaled using weighted RGBA blending over 2×2 pixel neighborhoods. This produces smoother gradients and reduces visible pixelation at the cost of larger file sizes; no additional computation time beyond the interpolation pass itself (≪1 second for typical sizes). Metadata key `tranZoom:render:i_pixels` stores the interpolation level. Use this when you want poster-quality output from moderate computation cost.
- **Frame interpolation** (`--i-frames`): Temporal interpolation applied to zoom animations to produce higher frame rates without computing additional fractal frames. The `--i-frames M` flag (valid range 0–7) inserts `M` interpolated frames between each real fractal-computed frame, multiplying the effective FPS by `(M+1)`. For example, `--fps 10 --i-frames 2` computes 10 real frames per second but outputs 30 total frames per second (10 real + 20 interpolated). Supports **linear interpolation** (weighted RGB blend between consecutive frames) and **quadratic interpolation** (three-point Lagrange curve using curr, next, next+1 frames for smoother acceleration). Quadratic mode is the default and produces more natural motion; it automatically falls back to linear for the final frame pair. This produces fluid animations from fewer expensive fractal computations; particularly effective for long zooms where computational cost dominates. The iteration depth (`max_iter`) is still computed per real frame using depth key frames, so quality remains consistent. Metadata key `tranZoom:zoom:frame:i_frames` stores the interpolation level. Use this when you want cinematic-smooth animations without the render time of computing every frame at full fractal precision.

#### Frame Representation
Expand Down Expand Up @@ -316,20 +316,23 @@ TransZoom 2.0 introduces two powerful interpolation modes that dramatically redu

**Purpose:** Upscale a single rendered fractal image to higher resolution without computing additional fractal pixels.

**How it works:** After the fractal escape-time computation finishes, tranZoom applies deterministic bicubic interpolation to the output image. For each interpolated pixel, a weighted RGBA blend is computed from the surrounding 2×2 neighborhood using sub-pixel fractional coordinates. This produces smooth gradients and reduces visible pixelation, particularly effective for poster-quality prints or detailed zoom views.
**How it works:** After the fractal escape-time computation finishes, tranZoom applies deterministic interpolation to the output image using a selectable resampling method (default: Lanczos). The interpolation algorithm blends RGBA pixel values from the neighborhood around each sub-pixel coordinate. This produces smooth gradients and reduces visible pixelation, particularly effective for poster-quality prints or detailed zoom views. The resampling method can be customized via `--resample` for different quality/speed trade-offs.

**Usage:**

```sh
poetry run tranz --palette electric image -s 1024 --i-pixels 2 mandel
poetry run tranz --palette electric image -s 1024 --i-pixels 2 --resample bilinear mandel # more stable output
```

This computes a 1024×1024 fractal (≈1.05M pixels), then upscales it to 3072×3072 (≈9.44M pixels) via bicubic interpolation. The fractal computation takes the same time as a normal 1024×1024 render; the interpolation pass adds only ≪1 second.
The first example computes a 1024×1024 fractal (≈1.05M pixels), then upscales it to 3072×3072 (≈9.44M pixels) via Lanczos interpolation. The second uses bilinear resampling, which is natively implemented and more guaranteed to be stable.

**Parameters:**

- **Flag:** `--i-pixels N` (valid range: 0–3)
- **Effect:** For `N > 0`, the final image dimensions are `(width * (N+1), height * (N+1))`
- **Flag:** `--resample METHOD` (resampling algorithm: bilinear, bicubic, lanczos, etc.; default: lanczos)
- **Effect:** Selects the interpolation algorithm used for pixel upscaling; bilinear is fastest and most stable, bicubic/lanczos offer smoother output
- **Examples:**
- `--i-pixels 0` → no interpolation (default)
- `--i-pixels 1` → 2× upscale (1024×1024 → 2048×2048)
Expand Down Expand Up @@ -415,7 +418,7 @@ The computation cost is that of 10×512×512 fractal renders; pixel interpolatio

##### Implementation notes

- **Pixel interpolation:** Uses `PIL.Image.resize()` with `Resampling.BICUBIC`; deterministic and reproducible across platforms
- **Pixel interpolation:** Uses `PIL.Image.resize()`
- **Frame interpolation:** Custom implementation in `zoom.py`; `InterpolatedFrameStream()` yields `bytes` (PNG-encoded frames) from an iterable of `(curr, next)` frame pairs; quadratic mode uses `QuadraticInterpolatedFrame()` with Lagrange polynomial blending
- **Color consistency:** Both modes respect the `ZoomColorNorm` applied to the base computation, so interpolated pixels/frames inherit the same color mapping as surrounding real data
- **Metadata roundtrip:** All interpolation settings are persisted in PNG text chunks and can be read back with `tranz image read`; re-rendering from a saved interpolated image will use the same settings
Expand Down Expand Up @@ -567,7 +570,8 @@ tranz [global flags] image [-w W] [-h H] [-s S] [--iter N] [--mark COORD] <mande
| `-w`/`--width` | Output image width in pixels (24–16384); NOTE: if `--i-pixels` is given, effective width = `w × (i+1)` | 1024 |
| `-h`/`--height` | Output image height in pixels (24–16384); NOTE: if `--i-pixels` is given, effective height = `h × (i+1)` | 1024 |
| `-s`/`--size` | Max pixel side; **overrides** `-w`/`-h` and scales the other dimension proportionally to match the frame aspect ratio; NOTE: if `--i-pixels` is given, effective size = `s × (i+1)` | None (use `-w`/`-h`) |
| `--i-pixels` | Number of interpolated pixels to add between each computed pixel (0–3); upscales final image via bicubic interpolation; `0` = no interpolation (default), `1` = 2× size, `2` = 3× size, `3` = 4× size; see [Pixel interpolation](#pixel-interpolation---i-pixels) | `0` |
| `--i-pixels` | Number of interpolated pixels to add between each computed pixel (0–3); upscales final image via interpolation; `0` = no interpolation (default), `1` = 2× size, `2` = 3× size, `3` = 4× size; see [Pixel interpolation](#pixel-interpolation---i-pixels) | `0` |
| `--resample` | Interpolation resampling method for pixel upscaling; available values: `bilinear`, `bicubic`, `lanczos`, and other PIL `Resampling` methods; bilinear is fastest and most stable | `lanczos` |
| `-i`/`--iter` | Override max iterations (depth); `1000`–4294967295 | automatic adaptive search |
| `--mark` | Draw a crosshair at this complex coordinate, formatted as `"(re, im)"` | None |
| `--mark-color` | Color of the crosshair; one of `black`, `white`, `red`, `green`, `blue`, `yellow`, `cyan`, `magenta` | `red` |
Expand All @@ -586,7 +590,8 @@ tranz [global flags] zoom [-w W] [-h H] [-s S] [-f FRACTAL] [-n STEPS] [--julia-
| `-w`/`--width` | Output image width in pixels (24–16384); NOTE: if `--i-pixels` is given, effective width = `w × (i+1)` | 512 |
| `-h`/`--height` | Output image height in pixels (24–16384); NOTE: if `--i-pixels` is given, effective height = `h × (i+1)` | 512 |
| `-s`/`--size` | Max pixel side; **overrides** `-w`/`-h` and scales proportionally; NOTE: if `--i-pixels` is given, effective size = `s × (i+1)` | None (use `-w`/`-h`) |
| `--i-pixels` | Number of interpolated pixels to add between each computed pixel (0–3); upscales final images via bicubic interpolation; see [Pixel interpolation](#pixel-interpolation---i-pixels) | `0` |
| `--i-pixels` | Number of interpolated pixels to add between each computed pixel (0–3); upscales final images via interpolation; see [Pixel interpolation](#pixel-interpolation---i-pixels) | `0` |
| `--resample` | Interpolation resampling method for pixel upscaling; available values: `bilinear`, `bicubic`, `lanczos`, and other PIL `Resampling` methods; bilinear is fastest and most stable | `lanczos` |
| `-f`/`--fractal` | Fractal type: `mandelbrot` or `julia` | `mandelbrot` |
| `--julia-re` | Real part of the Julia Set constant `c` | `'0.27334'` |
| `--julia-im` | Imaginary part of the Julia Set constant `c` | `'0.00742'` |
Expand Down
6 changes: 3 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion scripts/make_test_images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ H300="127/6100000000000000000000000000000000000000000000000000000000000000000000

# these are meant to stress the mandelbrot/julia math python/cython implementations
# 1: superficial Mandelbrot zoom, with set[IMAGINARY]
poetry run tranz --no-db --force --palette "lava" --set imaginary --set-palette "toxic" --no-date --no-hash --prefix "test-mandel-z-auto-seahorse" -o tests/data/images zoom -s 53 --i-pixels 2 auto " -0.7436499" "0.13188204" "227/193" "167/193" "131/43" --fps 1 --duration "31.7" --i-frames 1
poetry run tranz --no-db --force --palette "lava" --set imaginary --set-palette "toxic" --no-date --no-hash --prefix "test-mandel-z-auto-seahorse" -o tests/data/images zoom -s 53 --i-pixels 2 --resample bilinear auto " -0.7436499" "0.13188204" "227/193" "167/193" "131/43" --fps 1 --duration "31.7" --i-frames 1
# 2: ultra-deep Mandelbrot zoom, no set
poetry run tranz --no-db --force --palette "electric" --no-date --no-hash --prefix "test-mandel-z-auto-seeds300" -o tests/data/images zoom -s 31 --i-pixels 3 auto "$CX300" "$CY300" "$W300" "$H300" "43/41" --fps 1 --duration "10.1" --i-frames 1
# 3: ultra-deep Mandelbrot with mini-brot and set coloring
Expand Down
23 changes: 17 additions & 6 deletions src/tranzoom/cli/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,14 @@ class CleanupOutputFormat(enum.Enum):
# this should NOT change over metadata changes, as it is computed from raw pixel data
# PNG - really only change if core computation changes, so these are more important to be stable
SEAHORSE_TAIL_HASH: str = '525aaf4c4a58391f1386889a54d54dfb91f099050af5783f97322e1f33e8b275'
SUZANA_WAVE_HASH: str = '95a6acd116fb1ca043f089f093fb0a8c139ffb490a6a24be068fa474c8636871'
SUZANA_WAVE_HASH: str = 'c748e691dbbfbec2c7008cb902f608e99f11950be2f469f0231a276bc8dbf3a2'
# GIF - these may change for core computation, or if the animation frame machinery changes
SEAHORSE_ANIMATED_HASH: str = 'd9204b9c2aec64555ca7ce48226301684737cce8b673febe86629c2e8a36ae19'
T_GIF_SEAHORSE_HASH: str = 'ae35fa4c7834bbfec989548e6bceddd09b821bf883c007b475ff306d7fa286ee'
T_GIF_SEEDS_300_HASH: str = '1b67e38d95dd10cc5600371719c63654bf3f70c40644f3532787f5a8a915a84f'
T_GIF_JULIA_SUZANA_HASH: str = 'cb1253e362e25c25da208473db24172b97bd5aad048cf986589d744536060531'
T_GIF_JULIA_DRAGON_HASH: str = '8e761fe4b35f9132d04bb76a2ec5504308bd7322bb735404c6c4f10d47736f5d'
T_GIF_JULIA_BLOB_HASH: str = 'ff42442b7928169fa68c4b814b1f43e38c9e273d143ec5a53f3b016708bc137d'
T_GIF_SEAHORSE_HASH: str = 'b4b514074d358c97ec2440557f920329195f8b1fb6ba38285c6dcb06c368119a'
T_GIF_SEEDS_300_HASH: str = 'b94ceeda67d96a2ce9f79a122d387e366c80799258b4bb02b2b5d17f93cb5d0e'
T_GIF_JULIA_SUZANA_HASH: str = '22d7b81e71b5f7a8c04950b627050a3e466ff0d3241fb4f720999c17d57db571'
T_GIF_JULIA_DRAGON_HASH: str = 'f749955dc69b0c5282c75e5470f7b132824e0b4deb5af6e8c8339a4bea040a3b'
T_GIF_JULIA_BLOB_HASH: str = 'a50bd733d704f9e5d2e726035bcef87b606b5adfed1b506dfe5bb9d36d3b57bd'
# SHA of all the frame's data - like above: computation or animation frame machinery changes
TEST_IMAGE_DATA_HASHES: dict[str, tuple[int, str]] = {
# name: (number of frames, hash of all the frames)
Expand Down Expand Up @@ -142,6 +142,15 @@ class CleanupOutputFormat(enum.Enum):
'so 0 is no interpolation, 1 means add 1 interpolated pixel between every pair of pixels, etc'
),
)
IMAGE_INTERPOLATION_RESAMPLE_OPTION: typer.models.OptionInfo = typer.Option(
pixels.DEFAULT_RESAMPLING.name.lower(),
'--resample',
help=(
f'Interpolation resampling method; default is "{pixels.DEFAULT_RESAMPLING.name.lower()}"; '
'"bilinear" has the most stable results; "lanczos" is the most accurate but slowest; '
'available values: ' + ', '.join(sorted(repr(c.name.lower()) for c in pixels.Resampling))
),
)
IMAGE_ZOOM_WIDTH_OPTION: typer.models.OptionInfo = typer.Option(
frame.DEFAULT_ZOOM_SIZE,
'-w',
Expand Down Expand Up @@ -782,6 +791,7 @@ class TranZoomConfig(clibase.CLIConfig):
img_height: int = frame.DEFAULT_IMAGE_SIZE # both `image` and `zoom` use, different defaults
img_size: int | None = None # for `image` and `zoom` commands, overrides width/height if given
i_pixels: int = 0 # for `image` and `zoom` commands
resample: pixels.Resampling = pixels.DEFAULT_RESAMPLING # for `image` and `zoom` commands

max_iter: int | None = None # for `image` command, also `zoom auto`
mark_coords: str | None = None # for `image` command, also `zoom auto`
Expand Down Expand Up @@ -1085,6 +1095,7 @@ def MakeRenderParameters(
escaped_pal=config.pal,
set_pal=None if config.set_points is None else config.set_pal,
i_pixels=config.i_pixels,
resample=config.resample,
mark_re=_MPQ_ZERO if mark_coords is None else mark_coords[0][0],
mark_im=_MPQ_ZERO if mark_coords is None else mark_coords[0][1],
mark_color=None if mark_coords is None else config.mark_color,
Expand Down
Loading
Loading