fullseye

tb_qft2 — 2D typed op

tb_qft2: input → output

図は合成の入力 128×128 で実際に走らせた出力。左が入力、右が出力。点群は上から見た散布(明るさ = z)、1-D 列は折れ線、体積は z 方向の最大値投影、動画は中央フレーム、複素画像は振幅、絵にならない返り値は値そのもの。

つまみ a は出力を変えない(実測: 0.1 / 0.5 / 0.9 で同一)。

つまみ b は出力を変えない(実測: 0.1 / 0.5 / 0.9 で同一)。

段階(前置きの op → この op。左から順):

tb_qft2: stages

別の画像でも(合成シーン / 写真 / 硬貨。上段が入力、下段がその出力。つまみは既定):

tb_qft2: other inputs

使い方

Quaternion (hypercomplex) 2-D Fourier transform. → (H, W, 4) centred spectrum.

The colour analogue of ``complexops.cx_fft``: a colour image is transformed
as **one** hypercomplex signal rather than three unrelated real ones. The
kernel is ``exp(-mu * 2*pi*(u*x/W + v*y/H))`` for a unit pure quaternion
*mu* (default: the grey axis of RGB, Sangwine's choice), and because
quaternions do not commute the kernel can be applied on either side:

* ``side="left"``  — ``F[u,v] = sum_{x,y} E(x,y,u,v) * f[x,y]``
* ``side="right"`` — ``F[u,v] = sum_{x,y} f[x,y] * E(x,y,u,v)``

**The argument is required.** Left and right are not a sign convention. On
the fuzzer's ``(32, 32)`` dichromatic render they differ by
``max|F_L - F_R| = 19.11`` against a peak modulus of ``1045`` (1.8 % of full
scale) and on a random colour field by ``33.35`` against ``892.9`` (3.7 %; another seed
gives 34.05 against 892) —
with no exception and no NaN to mark the difference. **Mixing them across a
round trip is much worse**, because there the disagreement is not attenuated
by the spectrum's dynamic range: ``iqft2(qft2(q, "left"), "right")`` returns
an image whose error reaches ``1.113`` on data whose own range is ``0.9994``
— a completely different picture that still looks like a picture. (On the
grey-axis-dominated dichromatic render the same mistake costs only ``0.054``
against a range of ``1.076``, which is the dangerous case: a 5 % error is
exactly the size that survives a visual check.)

The spectrum is returned **centred** (DC at the array centre, via
``fftshift``), matching the convention ``complexops.cx_fft`` established for
the ``cimage`` sort. :func:`iqft2` un-centres before inverting, and the round
trip is exact: measured ``max|iqft2(qft2(q, s), s) - q| = 2.22e-15`` for both
sides on a standard-normal ``(32, 32, 4)`` field.

How it is computed, and why that is not a shortcut
--------------------------------------------------
Every quaternion splits as ``q = A + B*nu`` with ``A, B`` in the commutative
subfield generated by ``mu`` (the *symplectic decomposition*, Ell &
Sangwine 2007). The kernel commutes with ``A`` and *anti*-commutes past
``nu``, so the whole transform reduces to two ordinary complex FFTs — for
the left transform both with the standard kernel, for the right one of them
with the conjugate kernel. **That reduction is what makes left and right
differ**, and it is verified against a brute-force ``O(N^2)`` quaternion DFT
written straight from the definition, on a 4x4 image, for three different
``mu`` and both sides: the largest disagreement over all six combinations is
**8.2e-15**. The fast path is checked against the definition, not against
itself. The choice of the internal ``nu`` is likewise verified not to matter
(two different ``nu``, max difference 1.4e-14 — see :func:`_mu_basis`).

Honest accounting against the channelwise baseline
--------------------------------------------------
Because the decomposition above is *linear* in the channels, the QFT is a
fixed recombination of the three per-channel complex FFTs: rebuilding
``qft2(q, "left")`` from three ``numpy.fft.fft2`` calls on the R, G and B
planes agrees to ``max|err| = 1.14e-13``. So this transform **buys no
information a channelwise FFT does not already contain**, and this module
does not claim it does. It also does not buy speed — it moves four real
transforms' worth of data where the channelwise route moves three, and pays
for the symplectic pack/unpack on top: measured on ``(256, 256)``, best of
20, **8.246 ms against 3.409 ms, i.e. 2.42x slower** (run to run, 2.3x-2.4x). What it buys is that
the four numbers stay one algebraic object, so a rotor can be applied to the
spectrum and the colour meaning of ``mu`` survives the transform.

**Raises** ``ValueError``: *qimage* is not a valid ``(H, W, 4)`` field;
*side* is not ``'left'`` / ``'right'``; *mu* is not a finite non-zero
3-vector.

Typed bridge of the quat op qft2 into the 2-D evolution registry: the same implementation, called under the op(v, a, b) convention. This op has no tunable parameter; a and b are unused.

参考(サンプルデータ・文献)

Studio で試す

下のプログラムは実際に走ることを確かめてある(図と同じ入力)。Studio のヘルプではこのブロックがボタンになり、その場で読み込んで実行できる。

img_to_rgb 0.50 0.50
tb_rgb_to_quaternion 0.50 0.50
tb_qft2 0.50 0.50

実行できる例(この op を実際に呼ぶ検証済みサンプル)

次の例は元の台帳 op qft2 を呼ぶもの。この橋渡し op は同じ実装を fn(v, a, b) 規約に合わせただけなので、挙動はそのまま当てはまる(呼び出し形だけ違う)。

型が繋がる次の op(qimage を入力に取れる)

identity · tb_quaternion_to_rgb · tb_quat_norm · tb_quat_conjugate_image · tb_quat_normalize_image · tb_monogenic_amplitude · tb_monogenic_phase · tb_monogenic_orientation

同カテゴリ(typed)

tb_points_to_voxel · tb_estimate_point_normals · tb_iss_keypoints · tb_project_points · tb_render_point_depth · tb_statistical_outlier_removal · tb_radius_outlier_removal · tb_voxel_grid_downsample


Provenance: ops.py — 2D operator registry. この per-op ノートは tools/opdocs.py md が自動生成(手編集しない)。

© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.