MATLAB 刚体坐标变换:点、方向和逆变换为什么不能混用
先给每个量写上坐标系
把传感器点云换到设备坐标系时,同一个旋转矩阵可能用于点、速度方向和坐标轴。它们的尺寸都像是 3×1,但平移项的处理不同;再把变换方向写反,矩阵运算依然能够完成,结果却没有预期的几何意义。
本文固定一个契约:右手坐标系、列向量、左乘变换。所有公式都围绕 p_A = R_AB*p_B + t_AB 展开;不给无效旋转矩阵偷偷做 SVD 修复,也不用 inv 求刚体逆变换。
**环境与验证状态:**2026年10月30日在Windows 11、MATLAB R2025b(25.2.0.2998904)中完成原生运行。本文代码仅使用基础MATLAB矩阵运算、表格与绘图,不调用Robotics System Toolbox或Image Processing Toolbox函数。作者自检、独立复核测试及PNG/FIG/CSV/MAT导出回读检查均完成,批处理退出码0。结论限定于上述实际版本,不代表所有MATLAB版本或真实设备外参精度。
1 R_AB 的列到底表示什么
约定 p_B 是同一个几何点在 B 系中的坐标,p_A 是它在 A 系中的坐标:
p A = R A B p B + t A B . p_A=R_{AB}p_B+t_{AB}. pA=RABpB+tAB.
这个下标含义是从 B 表达转换为 A 表达 。R_AB 的三列依次为 B 系的 x、y、z 单位轴在 A 系中的表达;t_AB 是 B 系原点在 A 系中的位置。令 p_B=0,就得到 p_A=t_AB,这是检查平移含义的快捷方式。
若 p_B=[x_B;y_B;z_B],乘积 R_AB*p_B 就是把三根 B 系基向量分别乘以三个坐标,再相加。两个坐标系的长度单位必须一致,不能把毫米平移加到米坐标上。
不要拿别处的行向量公式直接代入。本文使用的齐次矩阵为
T A B = R A B t A B 0 1 , p A 1 = T A B p B 1 . T_{AB}=\begin{bmatrix}R_{AB}&t_{AB}\\0&1\end{bmatrix},\quad \begin{bmatrix}p_A\\1\end{bmatrix}=T_{AB}\begin{bmatrix}p_B\\1\end{bmatrix}. TAB=RAB0tAB1,pA1=TABpB1.
MathWorks 的 tform2rotm 文档也明确区分了这种前乘形式;这里只引用约定作对照,实际代码不调用该工具箱函数。MathWorks 齐次变换说明
2 用整数例子把方向确定下来
取 B 系绕共同 z 轴相对 A 系转过 +90°,并把 B 原点放在 A 系 [10;20;30]:
R A B = 0 − 1 0 1 0 0 0 0 1 , t A B = 10 20 30 . R_{AB}=\begin{bmatrix}0&-1&0\\1&0&0\\0&0&1\end{bmatrix},\qquad t_{AB}=\begin{bmatrix}10\\20\\30\end{bmatrix}. RAB= 010−100001 ,tAB= 102030 .
B 的 x 轴在 A 中朝 +y,B 的 y 轴在 A 中朝 −x。对于 p_B=[1;2;3],先组合基向量得到 [-2;1;3],再加原点位置,手算得到
p A = 8 21 33 . p_A=\begin{bmatrix}8\\21\\33\end{bmatrix}. pA= 82133 .
这三个整数同时是手算预期值和本次MATLAB原生运行得到的第一个点坐标,CSV回读也得到 [8,21,33]。
3 方向没有平移项
同一坐标系中的两点相减得到位移方向。设 v_B=q_B-p_B,则
v A = q A − p A = ( R A B q B + t A B ) − ( R A B p B + t A B ) = R A B v B . v_A=q_A-p_A=(R_{AB}q_B+t_{AB})-(R_{AB}p_B+t_{AB})=R_{AB}v_B. vA=qA−pA=(RABqB+tAB)−(RABpB+tAB)=RABvB.
平移抵消。因此,若 v_B=[1;0;0],手算应有 v_A=[0;1;0]。把平移也加进去会得到 [10;21;30],长度和方向都改变了。
在齐次形式中,点的第四个分量为 1,方向的第四个分量为 0。绘图时可以把方向箭头的尾部放到 B 原点,以便看清它指向哪里;那只是画图位置,不表示方向的分量也要加上 t_AB。
这里的方向是自由向量,不自动单位化。若输入是速度向量,旋转后仍保留相应大小,但这只讨论同一时刻的坐标表达变换,不包含运动参考系的角速度、原点速度等动力学项。
4 逆变换为什么不是只把平移取负
从原式移项:
p B = R A B T ( p A − t A B ) = R A B T p A − R A B T t A B . p_B=R_{AB}^{\mathsf T}(p_A-t_{AB}) =R_{AB}^{\mathsf T}p_A-R_{AB}^{\mathsf T}t_{AB}. pB=RABT(pA−tAB)=RABTpA−RABTtAB.
因此
T B A = T A B − 1 = R A B T − R A B T t A B 0 1 . T_{BA}=T_{AB}^{-1} =\begin{bmatrix}R_{AB}^{\mathsf T}&-R_{AB}^{\mathsf T}t_{AB}\\0&1\end{bmatrix}. TBA=TAB−1=RABT0−RABTtAB1.
逆平移必须先换到 B 系,不能简单填 -t_AB。对上面的点,正确逆变换恢复 [1;2;3];错误式 R_AB.'*p_A-t_AB 会得到 [11;-28;3]。这个反例也写入了本次已执行的作者测试断言。
对于严格的正交旋转,上式是精确的刚体逆公式。实际浮点矩阵可能只有近似正交,使用转置所得逆也相应是近似的,大坐标或大平移会放大非正交残差;后文用残差检查和往返测试约束数值实现,不能把它当成实际外参已经标定准确的证明。
5 连乘顺序用下标核对
已知 B 到 A 的 T_AB,以及 C 到 B 的 T_BC,则
p A = T A B ( T B C p C ) , T A C = T A B T B C . p_A=T_{AB}(T_{BC}p_C),\qquad T_{AC}=T_{AB}T_{BC}. pA=TAB(TBCpC),TAC=TABTBC.
这里的 p 在齐次等式中包含第四个分量。右侧变换先作用,内侧的 B 下标衔接起来;不能因为矩阵都是 4×4 就任意交换。
还可以在一个固定坐标系中检查主动操作的非交换性:把原点先沿 +x 平移 1,再绕 z 转 +90°,得到 [0;1;0];反过来先旋转原点、再平移,得到 [1;0;0]。这个操作顺序反例与上面的坐标表达契约分开使用,避免混淆"移动物体"与"更换表达坐标系"。
6 检查正交性还要检查行列式
右手三维刚体旋转应同时满足
R T R = I , det ( R ) = + 1. R^{\mathsf T}R=I,\qquad\det(R)=+1. RTR=I,det(R)=+1.
只查 det(R)=1 会放过 diag([2,0.5,1]):它把一个方向放大、另一个方向缩小,行列式仍为 1,却不保持距离。只查正交性则会放过 diag([-1,1,1]):它是反射,行列式为 −1,不符合本文两边均为右手系的旋转约定。
这里使用行列式确认近正交矩阵的取向,不把行列式用作通用奇异性测试;MathWorks 明确提醒,行列式的大小通常不能可靠反映矩阵是否接近奇异。MathWorks det 文档
实现默认要求 norm(R.'*R-eye(3),'fro') <= 1e-10 且 abs(det(R)-1) <= 1e-10。允许的 tol 限定在 10*eps 到 1e-6,防止随意放大阈值,把明显缩放或剪切也放进来。这是本文小型 double 示例的数值策略,不是普适的测量精度规范。输入中小于阈值的非正交残差仍可能存在,输出误差还随坐标与平移尺度变化,因此 tol 不是位置误差上界。
代码发现不合格矩阵便报错,不投影到"最近的旋转"。若数据需要修复,应另行说明来源、修复方法和几何代价。传感器数值标定与坐标外参也要分开:既有文章《MATLAB 线性标定与残差诊断 用反斜杠识别高 R 平方背后的曲率》的题目涉及数值标定;数值拟合良好本身不能确定两个坐标系的轴向、原点和变换方向,这里不推断旧文的具体实现。
7 完整实现
保存为 source/rigid_frame_transform.m。所有输入限定为有限、实数、非稀疏 double;R_AB 为 3×3,t_AB 为 3×1,点和方向按列存放为 3×N。两者的列数可以不同,也可分别传入 3×0 空数组。不会把行点集自动转置,避免掩盖调用错误。算术溢出产生非有限输出时也报错。
matlab
function [P_A,V_A,T_AB,T_BA,checks] = rigid_frame_transform(R_AB,t_AB,P_B,V_B,tol)
%RIGID_FRAME_TRANSFORM Right-handed, column-vector rigid frame contract.
% Points: P_A = R_AB*P_B + t_AB; directions: V_A = R_AB*V_B.
% R_AB columns are the B axes expressed in A; t_AB is B's origin in A.
% Inputs are real, finite, full doubles. Points/directions are 3-by-N.
% Empty 3-by-0 point and direction arrays are supported independently.
% tol is a numerical residual threshold, NOT a physical accuracy bound.
% For a near-orthogonal accepted R, T_BA is only an approximate inverse.
% Large coordinate or translation magnitudes can amplify that residual.
if nargin < 5
tol = 1e-10;
end
if ~(isa(tol,'double') && isreal(tol) && isscalar(tol) && ~issparse(tol) ...
&& isfinite(tol) && tol >= 10*eps && tol <= 1e-6)
error('rigid_frames:Tolerance','tol must be in [10*eps,1e-6].');
end
if ~validMatrix(R_AB) || ~isequal(size(R_AB),[3 3])
error('rigid_frames:RotationShape','R_AB must be a finite 3-by-3 double.');
end
if ~validMatrix(t_AB) || ~isequal(size(t_AB),[3 1])
error('rigid_frames:TranslationShape','t_AB must be a finite 3-by-1 double.');
end
if ~validMatrix(P_B) || size(P_B,1) ~= 3
error('rigid_frames:PointShape','P_B must be a finite 3-by-N double.');
end
if ~validMatrix(V_B) || size(V_B,1) ~= 3
error('rigid_frames:DirectionShape','V_B must be a finite 3-by-N double.');
end
orthogonalityError = norm(R_AB.'*R_AB-eye(3),'fro');
determinant = det(R_AB);
if ~isfinite(orthogonalityError) || ~isfinite(determinant) ...
|| orthogonalityError > tol || abs(determinant-1) > tol
error('rigid_frames:InvalidRotation', ...
'R_AB must satisfy orthogonality and determinant +1 checks.');
end
P_A = R_AB*P_B + repmat(t_AB,1,size(P_B,2));
V_A = R_AB*V_B;
T_AB = [R_AB t_AB; 0 0 0 1];
T_BA = [R_AB.' -R_AB.'*t_AB; 0 0 0 1];
if any(~isfinite(P_A(:))) || any(~isfinite(V_A(:))) ...
|| any(~isfinite(T_BA(:)))
error('rigid_frames:Overflow','Transformation produced nonfinite outputs.');
end
checks = struct('orthogonalityError',orthogonalityError, ...
'determinant',determinant,'tolerance',tol);
end
function ok = validMatrix(x)
ok = isa(x,'double') && isreal(x) && ~issparse(x) && ismatrix(x) ...
&& all(isfinite(x(:)));
end
源码中的"NOT RUN in the preparation workspace"注释记录最初云端备稿时的状态;为保持与本次实际测试文件一致,代码原样保留,当前运行结论以上述原生日志为准。
8 自检测试如何覆盖常见误用
保存为 source/test_rigid_frame_transform.m。**本节断言已在上述MATLAB R2025b环境执行完成。**手算整数例子检查方向和逆平移;直接 3×3 路径与函数返回的 4×4 齐次矩阵路径进行内部一致性检查,不能称作独立参照;非坐标轴旋转用确定角度构造;两级转换检查连乘;反射、缩放、剪切、空点集和维度错误单独覆盖。
对于合法旋转,任意两点之差满足 Δp_A=R_AB*Δp_B,因此长度保持。程序会对一组点的点对距离进行比较。数值测试使用明确的误差阈值,不把浮点相等与几何真实性混为一谈。
matlab
function test_rigid_frame_transform
% Authored assertions; NOT RUN in the preparation workspace.
R = [0 -1 0; 1 0 0; 0 0 1];
t = [10;20;30];
P = [1 4 -2 0; 2 -1 3 0; 3 5 1 0];
V = [1 0;0 1;0 0];
[PA,VA,T,Ti,checks] = rigid_frame_transform(R,t,P,V);
assert(isequal(PA(:,1),[8;21;33]));
assert(isequal(VA(:,1),[0;1;0]));
assert(isequal(R*V(:,1)+t,[10;21;30]));
assert(~isequal(VA(:,1),R*V(:,1)+t));
assert(checks.orthogonalityError == 0 && checks.determinant == 1);
% Internal consistency: returned T versus direct point/direction outputs.
homPoints = T*[P;ones(1,size(P,2))];
homDirections = T*[V;zeros(1,size(V,2))];
assertNear(homPoints(1:3,:),PA,1e-12);
assertNear(homDirections(1:3,:),VA,1e-12);
assert(isequal(homPoints(4,:),ones(1,size(P,2))));
assert(isequal(homDirections(4,:),zeros(1,size(V,2))));
back = Ti*[PA;ones(1,size(PA,2))];
assertNear(back(1:3,:),P,1e-12);
wrongInverse = R.'*PA(:,1)-t;
assert(isequal(wrongInverse,[11;-28;3]));
assert(~isequal(wrongInverse,P(:,1)));
assertNear(Ti*T,eye(4),1e-12);
assertNear(T*Ti,eye(4),1e-12);
assertNear(PA(:,2)-PA(:,1),R*(P(:,2)-P(:,1)),1e-12);
for i = 1:size(P,2)
for j = i:size(P,2)
assert(abs(norm(PA(:,i)-PA(:,j))-norm(P(:,i)-P(:,j))) < 1e-12);
end
end
% A non-axis-aligned deterministic proper rotation.
a = .37; b = -.52;
Rz = [cos(a) -sin(a) 0;sin(a) cos(a) 0;0 0 1];
Rx = [1 0 0;0 cos(b) -sin(b);0 sin(b) cos(b)];
Q = Rz*Rx;
[QA,QV,U,Ui] = rigid_frame_transform(Q,[.5;-2;3],P,V);
reference = U*[P;ones(1,size(P,2))];
recovered = Ui*[QA;ones(1,size(QA,2))];
assertNear(QA,reference(1:3,:),1e-12);
assertNear(recovered(1:3,:),P,1e-12);
assertNear(QV,Q*V,1e-12);
% Composition: subscript cancellation fixes order.
R_BC = [1 0 0;0 0 -1;0 1 0];
t_BC = [1;2;0];
[P_B,~,T_BC] = rigid_frame_transform(R_BC,t_BC,P,zeros(3,0));
P_A_twoSteps = rigid_frame_transform(R,t,P_B,zeros(3,0));
T_AC = T*T_BC;
P_A_oneStep = T_AC*[P;ones(1,size(P,2))];
assertNear(P_A_twoSteps,P_A_oneStep(1:3,:),1e-12);
% Active operations in one fixed frame: translation and rotation do not commute.
translation = [eye(3) [1;0;0];0 0 0 1];
rotation = [R zeros(3,1);0 0 0 1];
origin = [0;0;0;1];
assert(isequal(rotation*translation*origin,[0;1;0;1]));
assert(isequal(translation*rotation*origin,[1;0;0;1]));
[emptyP,emptyV] = rigid_frame_transform(R,t,zeros(3,0),zeros(3,0));
assert(isequal(size(emptyP),[3 0]) && isequal(size(emptyV),[3 0]));
mustError(@() rigid_frame_transform(diag([2 .5 1]),t,P,V),'rigid_frames:InvalidRotation');
mustError(@() rigid_frame_transform([1 .1 0;0 1 0;0 0 1],t,P,V),'rigid_frames:InvalidRotation');
mustError(@() rigid_frame_transform(diag([-1 1 1]),t,P,V),'rigid_frames:InvalidRotation');
mustError(@() rigid_frame_transform(zeros(3),t,P,V),'rigid_frames:InvalidRotation');
mustError(@() rigid_frame_transform(eye(2),t,P,V),'rigid_frames:RotationShape');
mustError(@() rigid_frame_transform(R,t.',P,V),'rigid_frames:TranslationShape');
mustError(@() rigid_frame_transform(R,t,P.',V),'rigid_frames:PointShape');
mustError(@() rigid_frame_transform(R,t,P,[0;NaN;0]),'rigid_frames:DirectionShape');
mustError(@() rigid_frame_transform(R,[0;Inf;0],P,V),'rigid_frames:TranslationShape');
mustError(@() rigid_frame_transform(single(R),t,P,V),'rigid_frames:RotationShape');
mustError(@() rigid_frame_transform(R,t,P+1i,V),'rigid_frames:PointShape');
mustError(@() rigid_frame_transform(R,t,P,V,1),'rigid_frames:Tolerance');
mustError(@() rigid_frame_transform(R,t,P,V,0),'rigid_frames:Tolerance');
mustError(@() rigid_frame_transform(eye(3),[realmax;0;0], ...
[realmax;0;0],zeros(3,0)),'rigid_frames:Overflow');
fprintf('test_rigid_frame_transform: all authored assertions completed.\n');
end
function assertNear(a,b,tol)
assert(isequal(size(a),size(b)));
assert(all(abs(a(:)-b(:)) <= tol));
end
function mustError(f,id)
try
f();
catch ex
assert(strcmp(ex.identifier,id),'Unexpected error: %s',ex.identifier);
return
end
error('test_rigid_frames:MissingError','Expected error %s.',id);
end
9 原生三维图和导出脚本
保存为 source/demo_rigid_frame_transform.m。左图同时显示 A、B 两套坐标轴及三个点,所有位置均用 A 坐标表达;右图放大 B 原点附近,区分"点的位置"和"方向箭头的放置位置"。坐标轴使用红、绿、蓝,数据轴等比例,显示范围根据几何范围加边距计算。本次已生成1936×1103的PNG并核对标题、图例和坐标方向;左图部分标签距离较近,右图的方向箭头与B系x轴同向重叠,需结合图例阅读。

图例中的方向箭头为便于阅读显示成 3 倍长度,数据变量 V_A 本身没有缩放。CSV 保存每个点在两套坐标系中的坐标;MAT 另外保存变换矩阵、方向、数值检查量及实际运行版本。脚本不读取设备或真实标定文件。
matlab
function artifacts = demo_rigid_frame_transform(outDir)
% Native MATLAB diagram and exports. Authored here, NOT RUN here.
if nargin < 1
outDir = fullfile(pwd,'rigid_native_output');
end
if ~exist(outDir,'dir')
mkdir(outDir);
end
test_rigid_frame_transform;
R_AB = [0 -1 0;1 0 0;0 0 1];
t_AB = [10;20;30];
P_B = [1 4 -2;2 -1 3;3 5 1];
V_B = [1;0;0];
[P_A,V_A,T_AB,T_BA,checks] = rigid_frame_transform(R_AB,t_AB,P_B,V_B);
recovered = T_BA*[P_A;ones(1,size(P_A,2))];
assert(all(abs(recovered(1:3,:)-P_B) < 1e-12,'all'));
data = table((1:size(P_B,2))',P_B(1,:)',P_B(2,:)',P_B(3,:)', ...
P_A(1,:)',P_A(2,:)',P_A(3,:)', ...
'VariableNames',{'pointId','x_B','y_B','z_B','x_A','y_A','z_A'});
fig = figure('Color','w','Position',[80 80 1350 620]);
layout = tiledlayout(fig,1,2,'TileSpacing','loose','Padding','loose');
ax1 = nexttile(layout); hold(ax1,'on');
drawFrame(ax1,eye(3),zeros(3,1),'A',7);
drawFrame(ax1,R_AB,t_AB,'B',7);
plot3(ax1,[0 t_AB(1)],[0 t_AB(2)],[0 t_AB(3)],'k--', ...
'LineWidth',1.2,'HandleVisibility','off');
plot3(ax1,P_A(1,:),P_A(2,:),P_A(3,:),'ko','MarkerFaceColor',[1 .6 .1], ...
'MarkerSize',7,'HandleVisibility','off');
for k = 1:size(P_A,2)
text(ax1,P_A(1,k)+.5,P_A(2,k)+.5,P_A(3,k)+.5,sprintf('p%d',k), ...
'Clipping','off');
end
title(ax1,'Frames and points, all coordinates expressed in A');
configureAxes(ax1);
fitLimits(ax1,[zeros(3,1) 8*eye(3) t_AB ...
repmat(t_AB,1,3)+R_AB*(8*eye(3)) P_A],2);
ax2 = nexttile(layout); hold(ax2,'on');
drawFrame(ax2,R_AB,t_AB,'B',3);
pointEnd = P_A(:,1);
vectorEnd = t_AB + 3*V_A;
plot3(ax2,[t_AB(1) pointEnd(1)],[t_AB(2) pointEnd(2)], ...
[t_AB(3) pointEnd(3)],'k-o','LineWidth',1.6, ...
'DisplayName','R_{AB} p_B from B origin');
quiver3(ax2,t_AB(1),t_AB(2),t_AB(3),3*V_A(1),3*V_A(2),3*V_A(3), ...
0,'Color',[.7 0 .7],'LineWidth',2,'MaxHeadSize',.4, ...
'DisplayName','v_A shown at B origin (length x3)');
text(ax2,pointEnd(1)+.25,pointEnd(2)+.25,pointEnd(3)+.25, ...
'p1_A = [8;21;33]','Clipping','off');
title(ax2,'Zoom: placing an arrow does not translate its components');
configureAxes(ax2);
fitLimits(ax2,[t_AB repmat(t_AB,1,3)+R_AB*(3.8*eye(3)) ...
pointEnd vectorEnd],1.5);
legend(ax2,'show','Location','southoutside');
title(layout,'Rigid frames: column vectors and right-handed axes');
drawnow;
meta = struct('matlabVersion',version,'release',version('-release'), ...
'generatedAt',datestr(now,30),'contract','p_A=R_AB*p_B+t_AB', ...
'lengthUnit','arbitrary consistent length unit');
artifacts = struct('png',fullfile(outDir,'rigid_frames.png'), ...
'fig',fullfile(outDir,'rigid_frames.fig'), ...
'csv',fullfile(outDir,'rigid_frames.csv'), ...
'mat',fullfile(outDir,'rigid_frames.mat'));
exportgraphics(fig,artifacts.png,'Resolution',180);
savefig(fig,artifacts.fig);
writetable(data,artifacts.csv);
save(artifacts.mat,'R_AB','t_AB','P_B','P_A','V_B','V_A', ...
'T_AB','T_BA','checks','data','meta');
disp(data);
disp(checks);
disp(meta);
end
function drawFrame(ax,R,o,label,len)
colors = [0.85 0.1 0.1;0.1 0.55 0.15;0.1 0.3 0.85];
names = {'x','y','z'};
for k = 1:3
v = len*R(:,k);
quiver3(ax,o(1),o(2),o(3),v(1),v(2),v(3),0, ...
'Color',colors(k,:),'LineWidth',1.6,'MaxHeadSize',.3, ...
'HandleVisibility','off');
endpoint = o + 1.08*v;
text(ax,endpoint(1),endpoint(2),endpoint(3), ...
sprintf('%s_%s',names{k},label),'Color',colors(k,:), ...
'Clipping','off');
end
text(ax,o(1),o(2),o(3),[' O_' label],'Clipping','off');
end
function configureAxes(ax)
grid(ax,'on'); axis(ax,'equal'); view(ax,38,24);
xlabel(ax,'x_A'); ylabel(ax,'y_A'); zlabel(ax,'z_A');
end
function fitLimits(ax,points,margin)
lo = min(points,[],2)-margin;
hi = max(points,[],2)+margin;
xlim(ax,[lo(1) hi(1)]); ylim(ax,[lo(2) hi(2)]); zlim(ax,[lo(3) hi(3)]);
end
10 最小复现命令与验收
保存本节之前三个代码块为对应文件,分别放入 source 目录;将独立测试文件放入 review 目录。进入稿件目录,准备有基础 MATLAB R2025b 的授权环境。本次实际执行的包装脚本先调用相同作者测试与独立测试,再运行示例并回读四种导出。下面给出等价的最小复现命令,输出目录必须事先不存在:
bash
matlab -batch "addpath('source','review'); out='native_results_20261030'; assert(~exist(out,'file')); mkdir(out); diary(fullfile(out,'run.log')); disp(version); ver; test_rigid_frame_transform; test_rigid_frames_independent; demo_rigid_frame_transform(out); diary off"
没有独立测试附件时,先只运行作者测试与示例,不得因此宣称独立复核通过。遇到错误时保留日志和调用栈,修复后换一个输出目录。
在 MATLAB 中将 source 加到路径,执行 test_rigid_frame_transform,再执行 demo_rigid_frame_transform。保留实际版本、完整日志、源文件和图表数据。逐一核对 [8;21;33]、无平移的 [0;1;0]、正确逆变换和错误逆变换的区分,以及变换连乘顺序。
图形验收要看坐标轴的正方向、图例、标签、等比例和是否裁切;不能仅凭"图看着像转了 90°"替代数值测试。独立复核者还需检查坐标契约和非法输入策略,并核对最终正文、源文件与实测版本的一致性。
2026-10-28 在 Python 3.12.14 中实际运行了独立标量几何参考:固定随机种子 20261028,共 1000 组旋转、点和平移,检查逆变换、距离保持和复合顺序;参考计算最大逆变换坐标误差为 5.684341886080802e-14。这是 Python 双精度参考值,不是 MATLAB 实测误差,也不代表设备外参精度。另运行了近正交与大平移反例。2026年10月30日的MATLAB原生记录另行保存:test_rigid_frame_transform: all authored assertions completed.、test_rigid_frames_independent: all reviewer assertions completed.,随后输出 PASS matlab_native_tests_and_artifact_roundtrip PNG=1936x1103,批处理退出码0。导出的三个点为 [8;21;33]、[11;24;35]、[7;18;31],方向为 [0;1;0];MAT和CSV回读坐标一致,FIG可重新打开。日志中的本例正交误差0、行列式1,不代表所有浮点输入均精确正交。Code Analyzer对核心函数、作者测试、独立测试没有诊断,示例有两条关于datestr/now改用datetime的样式建议,不影响本次运行。图像也经过目视核对。
继续阅读
若还在整理采集数据,可接着看已发布的MATLAB ADC 码流触顶诊断。触顶检查处理数值边界,本篇处理几何表达,两者不能互相代替。
结尾
刚体变换的公式很短,但每个量都带着坐标系和几何类型。先写清 R_AB、t_AB 的方向,再区分点与自由向量,逆变换与连乘就有了可核对的依据。
你现在保存的外参文件,是否明确写了"从哪一系到哪一系"、列向量还是行向量、长度单位和左右手约定?如果把 B 原点与 B 的三根单位轴各变换一次,结果能否对应设备上实际可确认的位置与正方向?