tb_quat_color_rotate — 2D typed op

• 데이터 종류: qimage → qimage

• 호출: fullseye.apply(img, "tb_quat_color_rotate", a=0.5, b=0.5)(2-D 는 이미지 1 장 + 스칼라 노브 2 개 a,b∈[0,1] 모델)

tb_quat_color_rotate: 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_quat_color_rotate: 단계 (문서 사이트)

다른 영상에서도(합성 장면 / 사진 / 동전. 윗줄이 입력, 아랫줄이 그 출력. 노브는 기본값):

▸ tb_quat_color_rotate: 다른 입력 이미지 (문서 사이트)

사용법

RGB 축을 기준으로 모든 픽셀의 색을 회전: 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 — _require_direction on the axis, and an explicit unit-norm assertion on the finished rotor (tolerance _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.

참고(샘플 데이터·문헌)

• 샘플 데이터 카탈로그(DL URL / 라이선스) —— 2-D 는 skimage.data(BSD/public)+ 합성, 3-D 는 실데이터 소스(Stanford/PDS 등)의 DL URL.

• 연산자의 내력·참고문헌 —— 이 연산자 족의 바탕이 된 연구/기법의 출처.

Studio에서 시도하기

아래 프로그램은 실제로 실행됨을 확인했습니다(그림과 같은 입력). Studio 도움말에서는 이 블록이 버튼이 되어 즉시 불러와 실행할 수 있습니다.

img_to_rgb 0.50 0.50
tb_rgb_to_quaternion 0.50 0.50
tb_quat_color_rotate 0.50 0.50

▸ 이 파이프라인 불러오기  ·  불러와서 실행

실행 가능한 예제(이 연산자를 실제로 호출하는 검증된 샘플)

아래 예제는 원래의 대장 op quat_color_rotate를 호출한다. 이 브리지 op는 같은 구현을 fn(v, a, b) 규약에 맞춘 것뿐이므로 동작은 그대로 적용된다(호출 형식만 다름).

• quaternion_monogenic — py -3.11 examples/quaternion_monogenic.py

타입이 이어지는 다음 연산자(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 연산자 레지스트리. 이 op 노트는 tools/opdocs.py md 가 자동 생성합니다(직접 편집하지 마세요).*

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