typed op• Data kinds: qimage → qimage
• Call: fullseye.apply(img, "tb_quat_color_rotate", a=0.5, b=0.5) (the 2-D model is one image plus two scalar knobs a,b∈[0,1])
Rotate every pixel's colour about an RGB axis: `q x conj(q)`. → (H, W, 4).
The operation a complex pixel cannot express. `axis_rgb` is a direction in
RGB space and `angle_rad` the rotation about it; the rotor
`q = cos(a/2) + sin(a/2) * axis` is built with
`pose_quat.axis_angle_to_quat` and the conjugation is applied to the vector
part of every pixel, leaving the scalar part untouched (a conjugation cannot
move it).
Exactness and what it is worth
------------------------------
The conjugation is applied through the `3x3` matrix from
`pose_quat.quat_to_hom_mat3d` rather than by two per-pixel Hamilton
products, because for a `(512, 512)` image that is 500k quaternion
multiplications versus one `einsum`. The two are the *same map*, measured:
against per-pixel `pose_quat.quat_rotate_point_3d` the agreement is
`4.44e-16, the round trip rotate(rotate(q, ax, a), ax, -a)` returns
`q to 2.22e-16, and the colour magnitude is preserved to 2.22e-16`.
The matrix identity is also the honest limit of the *capability* claim.
`SO(3)` and the unit quaternions are isomorphic, so **a 3x3 orthogonal
colour matrix does exactly this and nothing is lost by using one** — measured
against an explicit `Rz(30 deg), the agreement is 2.22e-16`. What a
quaternion buys is 4 numbers instead of 9, exact closure under composition,
and `slerp`. Measured over 100,000 random small rotations composed in
sequence, the quaternion (renormalised each step, 4 divisions) drifts from
unit norm by 0.0 while the matrix (composed by multiplication, not
re-orthonormalised) drifts to `|R^T R - I| = 4.33e-14`.
**That advantage is real but it is nearly nothing, and an earlier revision of
this file oversold it by four orders of magnitude.** The same measurement
then read `4.4e-10` for the matrix, which looked like a decisive argument
for quaternions. It was not an argument about quaternions at all: it was the
`pose_quat` defect described below, feeding a slightly non-orthogonal
matrix into every one of the 100,000 steps. With that fixed the honest figure
is `4.33e-14`, i.e. ordinary rounding over 100k products. The lesson is the
repository's own: a number that flatters the thing you are building is the
one to re-measure first.
What a *channelwise* pipeline — three independent scalar filters, which is
what running the complex ops on R, G and B separately means — cannot do is
this operation at all: it never mixes channels, so it cannot turn red towards
green. Pure red rotated 90 degrees about the blue axis comes back as
`(-2.2e-16, 1.0, 0.0)`; no per-channel gain can put anything in the green
channel, because it starts at zero. That is the comparison in
`tests/test_quatimage.py`, and it is the one that is decisive.
Two traps in the rotor, both refused here regardless of the dependency
---------------------------------------------------------------------
`pose_quat used to normalise as n / (norm + 1e-12)`. A zero axis
then returned `[cos(a/2), 0, 0, 0], which quat_to_hom_mat3d`
re-normalised to the identity: a rotation request silently became a no-op.
Worse, at `angle_rad = pi that same path produced [0, 0, 0, 0]`, whose
normalisation was `0/(0+1e-12) = 0` and whose matrix was again the identity
— a *180-degree* colour rotation silently becoming a copy. Both were reported
and have since been fixed in `pose_quat` itself (zero length now
raises; `axis_angle_to_quat(0, 0, 1, pi)` now returns exactly
`[0, 0, 0, 1]` with norm 1, where it used to return norm 0.9999999999990).
This operator nevertheless keeps both of its own guards —
:func:_require_direction on the axis, and an explicit unit-norm assertion
on the finished rotor (tolerance :data:_UNIT_TOL). A check that only holds
while a dependency behaves is not a check, and the caller of this module
should get this module's error message. A genuine pi rotation still works:
pure red about the blue axis gives `(-1.0, 1.2e-16, 0.0)`.
Raises `ValueError: *qimage* is not a valid (H, W, 4)` field;
*axis_rgb* is not a finite non-zero 3-vector; *angle_rad* is not a finite
real scalar; the constructed rotor is not unit norm.
Typed bridge of the quat op `quat_color_rotate 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.
• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).
• Operator provenance and references — the sources of the research/methods this op family came from.
• (none yet)
qimage as input)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_angle_3points · tb_project_points · tb_render_point_depth · tb_statistical_outlier_removal · tb_radius_outlier_removal
*Provenance: ops.py — 2D operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*
© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.